Skip to content

Jminnetian/askskills#1410

Open
jsourcebot wants to merge 25 commits into
mainfrom
jminnetian/askskills
Open

Jminnetian/askskills#1410
jsourcebot wants to merge 25 commits into
mainfrom
jminnetian/askskills

Conversation

@jsourcebot

@jsourcebot jsourcebot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

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

    • Added Ask Sourcebot skills for creating, importing, sharing, syncing, and using reusable chat instructions.
    • Added a new Skills settings area for managing personal and workspace skills.
    • Chat now supports skill-based slash commands and automatic skill loading during conversations.
    • Added source status and import-from-repository support for synced skills.
  • Bug Fixes

    • Improved navigation guards and settings layout behavior.
    • Fixed tooltip, dropdown, and sidebar interactions for smoother navigation.

jsourcebot added 24 commits July 1, 2026 16:20
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.
@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This 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 load_skill tool with a registry, slash-command resolution in chat, composer UI, editor/management pages, and repo-source sync status tracking, plus supporting fixes.

Changes

Agent Skills Feature

Layer / File(s) Summary
Database schema and scope helpers
packages/db/prisma/migrations/*, packages/db/prisma/schema.prisma, packages/db/src/index.ts
Adds AgentSkill/AgentSkillAdoption models, indexes, and Prisma scope/visibility query helpers.
Skill types, parsing, catalog and repo access
packages/web/src/ee/features/chat/skills/types.ts, .../commandCatalog.ts, .../sourceRepoAccess.ts, tests
Zod schemas, markdown frontmatter parsing, slug normalization, command catalog builders, and repo-access filtering.
Skill server actions
packages/web/src/ee/features/chat/skills/actions.ts, commands.server.ts, errorCodes.ts, tests
CRUD, publish/convert scope, adopt/unadopt, source freshness/refresh, and new error codes.
Auto-invocation registry and load_skill tool
.../skills/registry.ts, .../skills/loadSkillTool.ts, .../skillAnalytics.ts, agent.ts, posthogEvents.ts
Registry resolution, fail-closed tool, analytics event, and <agent_skills> prompt wiring.
Slash-command resolution and message types
.../skills/commandResolution.ts, features/chat/commands/*, features/chat/types.ts, features/chat/utils.ts, ee/chat/route.ts, mcpOAuthDraft.ts
Materializes commands into expanded instructions and extends message/mention types.
Chat composer UI
chatBox.tsx, keyboard.ts, suggestionsBox.tsx, useSuggestions*.ts, chatThread*.tsx, detailsCard.tsx, loadSkillToolComponent.tsx
Slash-command suggestions, mention rendering, and askCommands threaded through chat pages/panels.
Skill editor and import
personalSkillEditorPage.tsx, skillInstructionsEditor.tsx, importFromRepoDialog.tsx, account editor pages
Editor page, Slate instructions editor with file mentions, and repo markdown import.
Management pages and workspace UI
skillsPage.tsx, settingsContentFrame.tsx, settings/skills/page.tsx, workspaceSharedSkillsManager.tsx, nav/connectors updates
Personal/shared management UI, workspace shared-skills manager, and settings navigation entries.
Source-sync status API
getFileSourceApi.ts, schemas.ts, sourceStatus/route.ts, client.ts, openapi spec
Blob-SHA resolution and sourceStatus endpoint used to detect stale synced skills.
Navigation guard and supporting fixes
useUnsavedChangesGuard.ts, layout.tsx, prismaErrors.ts, tooltip.tsx, searchModeSelector.tsx
Generalized unsaved-changes guard and unrelated small fixes.

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

Possibly related PRs

Suggested labels: sourcebot-team

Suggested reviewers: brendan-kellam, msukkari

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title looks like a branch or namespace and doesn’t describe the actual PR change. Use a concise descriptive title such as 'Add Ask Sourcebot skills management and auto-invoke support'.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch jminnetian/askskills

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.

❤️ Share

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

@mintlify

mintlify Bot commented Jul 2, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
sourcebot 🟢 Ready View Preview Jul 2, 2026, 1:39 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

License Audit

⚠️ Status: PASS

Metric Count
Total packages 2225
Resolved (non-standard) 12
Unresolved 0
Strong copyleft 0
Weak copyleft 38

Weak Copyleft Packages (informational)

Package Version License
@img/sharp-libvips-darwin-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-darwin-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.0.5 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-ppc64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-riscv64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-s390x 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linux-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-arm64 1.2.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.0.4 LGPL-3.0-or-later
@img/sharp-libvips-linuxmusl-x64 1.2.4 LGPL-3.0-or-later
@img/sharp-wasm32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-wasm32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later AND MIT
@img/sharp-win32-arm64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-ia32 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.33.5 Apache-2.0 AND LGPL-3.0-or-later
@img/sharp-win32-x64 0.34.5 Apache-2.0 AND LGPL-3.0-or-later
axe-core 4.10.3 MPL-2.0
lightningcss 1.32.0 MPL-2.0
lightningcss-android-arm64 1.32.0 MPL-2.0
lightningcss-darwin-arm64 1.32.0 MPL-2.0
lightningcss-darwin-x64 1.32.0 MPL-2.0
lightningcss-freebsd-x64 1.32.0 MPL-2.0
lightningcss-linux-arm-gnueabihf 1.32.0 MPL-2.0
lightningcss-linux-arm64-gnu 1.32.0 MPL-2.0
lightningcss-linux-arm64-musl 1.32.0 MPL-2.0
lightningcss-linux-x64-gnu 1.32.0 MPL-2.0
lightningcss-linux-x64-musl 1.32.0 MPL-2.0
lightningcss-win32-arm64-msvc 1.32.0 MPL-2.0
lightningcss-win32-x64-msvc 1.32.0 MPL-2.0
Resolved Packages (12)
Package Version Original Resolved Source
@react-grab/cli 0.1.23 UNKNOWN MIT GitHub repo (aidenybai/react-grab monorepo LICENSE)
@react-grab/cli 0.1.29 UNKNOWN MIT GitHub repo (aidenybai/react-grab monorepo LICENSE)
@react-grab/mcp 0.1.29 UNKNOWN MIT GitHub repo (aidenybai/react-grab monorepo LICENSE)
codemirror-lang-elixir 4.0.0 UNKNOWN Apache-2.0 GitHub repo (livebook-dev/codemirror-lang-elixir LICENSE)
element-source 0.0.3 UNKNOWN MIT LICENSE file in published npm package (via unpkg)
khroma 2.1.0 UNKNOWN MIT GitHub repo (fabiospampinato/khroma LICENSE)
lezer-elixir 1.1.2 UNKNOWN Apache-2.0 GitHub repo (livebook-dev/lezer-elixir LICENSE)
map-stream 0.1.0 UNKNOWN MIT GitHub repo (dominictarr/map-stream LICENSE)
memorystream 0.3.1 UNKNOWN MIT npm registry licenses field ([{type:MIT}])
pause-stream 0.0.11 ["MIT","Apache2"] (MIT OR Apache-2.0) extracted from object/array license field
posthog-js 1.369.0 SEE LICENSE IN LICENSE Apache-2.0 GitHub repo LICENSE file (Apache-2.0 primary, with MIT for vendored portions)
valid-url 1.0.9 UNKNOWN MIT GitHub repo (ogt/valid-url LICENSE)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 win

New "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 hasAskEntitlement is true — non-entitled orgs get no entry at all. But SkillsEntitlementMessage (rendered by /settings/skills when !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/skills for such users, and the deep-link source (workspace admin table) is likewise gated behind isAskAgentAvailable, 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 win

Per-turn budget check should run after command materialization. The current guard only measures the raw user text, so a short /skill can expand into much larger instructions and bypass ATTACHMENT_MAX_TURN_TEXT_BYTES before the message is sent and persisted. Move materializeCommandMessageTexts ahead 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 win

Duplicate repo/path/ref validation logic with getFileSourceForRepo.

The repo lookup, isPathValid/isGitRefValid checks, and git-instance/gitRef setup (lines 121-143) are copy-pasted from getFileSourceForRepo (lines 30-48). Consider extracting a shared helper (e.g. resolveRepoAndGitRef) returning either the validated { repo, git, gitRef } or the appropriate ServiceError, 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 win

Compute blobSha from fileContent instead of a second rev-parse call. 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 if gitRef moves 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 value

Consider collapsing the two near-identical titleDetail branches.

The titleDetail-present and titleDetail-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 the titleDetail span 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 | 🔵 Trivial

Missing coverage for bypass() and the one-shot resolve() latch.

Good coverage of the beforeunload and controlled/confirm push paths, but there's no test exercising bypass() (used for save-then-redirect flows) or the decisionMadeRef one-shot guard in resolve(). Given bypass() has a state-reset bug (see useUnsavedChangesGuard.ts comment), 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 win

Redundant index alongside unique constraint.

AgentSkill_orgId_visibility_scopeId_idx (orgId, visibility, scopeId) is a strict column-prefix of the unique index AgentSkill_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_key for 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 | 🔵 Trivial

Inconsistent 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 the isServiceError() helper already imported at the top. Using isServiceError() 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

skillSourceInvalid and skillNotSynced share the same errorCode.

Both errors use ErrorCode.INVALID_REQUEST_BODY despite representing distinct conditions (missing source link vs. now-invalid source content) and different HTTP status codes (400 vs 422). Clients branching on errorCode alone 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 win

Duplicate fetch pattern for manageable shared skills.

getSharedAgentSkill and updateSharedAgentSkillFromSource both call requireManageableSharedSkill (which already does a findFirst with enabled: true) and then immediately re-fetch the same row with a second findFirst to pull additional columns. updateSharedAgentSkill avoids this by using the fields already returned from requireManageableSharedSkill. Consider making the select in requireManageableSharedSkill generic/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 | 🔵 Trivial

Un-sharing a skill silently drops it for all other adopters.

When the creator calls makeSharedAgentSkillPersonal, the shared AgentSkill row is deleted (line 894-898). Per the migration, AgentSkillAdoption rows cascade-delete on AgentSkill removal, 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.error instead of the project's structured logger.

These three functions log failures with console.error, while the surrounding chat/skills tests mock createLogger from @sourcebot/shared, suggesting other modules in this feature use the structured logger. Using console.error here 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 value

Redundant 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 value

Non-null assertions on skill!.id for edit modes.

saveMode guarantees skill !== null when editShared/editPersonal execute, but TypeScript can't infer that through the map-lookup pattern, hence the ! assertions. Functionally safe given the current saveMode derivation, but fragile to future refactors of saveModeConfig/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 value

Curly-brace bodies should be on their own line.

Both onOpenChange handlers place the if body on the same line, unlike the rest of the file which consistently formats multi-line.

As per coding guidelines: "Always use curly braces for if statements, 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 win

Missing coverage for the owner-(non-creator) "make personal" path.

All make-personal tests use sharedSkill/syncedSkill, which are treated as user-created (isCreatedByUser not overridden to false for an owner acting on someone else's skill). This gap would have caught the stale-state issue flagged in skillsPage.tsx's handleMakePersonal.

🤖 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 value

Curly-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 if statements, 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 win

Repo search fires a request on every keystroke.

repoQuery drives the useQuery key directly with no debounce, so each keystroke issues a new listRepos call (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

flag param is accepted but not used to build the update payload.

data: { autoEnrolled: checked } is hardcoded, ignoring flag. Works today since OrgSkillFlagKey has 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 | 🔵 Trivial

Skill catalog gating and load_skill wiring look correct.

Entitlement/identity gating mirrors the existing MCP-tool pattern, and alwaysActiveTools is defined once so both the initial activeTools and every prepareStep rebuild stay in sync automatically. Reasonable defense-in-depth against the registry snapshot going stale (re-resolved in resolveAutoInvocableSkill).

One minor unbounded-growth consideration worth keeping in mind operationally: buildSkillRegistry loads 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

📥 Commits

Reviewing files that changed from the base of the PR and between e706330 and 20f5b03.

⛔ Files ignored due to path filters (1)
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing (98)
  • CHANGELOG.md
  • docs/api-reference/sourcebot-public.openapi.json
  • packages/db/prisma/migrations/20260624202848_refactor_agent_skills/migration.sql
  • packages/db/prisma/migrations/20260626120000_remove_featured_from_agent_skill/migration.sql
  • packages/db/prisma/migrations/20260627223058_add_agent_skill_source_provenance/migration.sql
  • packages/db/prisma/schema.prisma
  • packages/db/src/index.ts
  • packages/web/package.json
  • packages/web/src/app/(app)/@sidebar/components/settingsSidebar/nav.tsx
  • packages/web/src/app/(app)/askgh/[owner]/[repo]/components/landingPage.tsx
  • packages/web/src/app/(app)/askgh/[owner]/[repo]/page.tsx
  • packages/web/src/app/(app)/chat/[id]/page.tsx
  • packages/web/src/app/(app)/chat/chatLandingPage.tsx
  • packages/web/src/app/(app)/chat/components/landingPageChatBox.tsx
  • packages/web/src/app/(app)/chat/layout.tsx
  • packages/web/src/app/(app)/components/searchModeSelector.tsx
  • packages/web/src/app/(app)/layout.tsx
  • packages/web/src/app/(app)/settings/accountAskAgent/skills/[skillId]/page.tsx
  • packages/web/src/app/(app)/settings/accountAskAgent/skills/new/page.tsx
  • packages/web/src/app/(app)/settings/accountAskAgent/workspaceSkills/[skillId]/page.tsx
  • packages/web/src/app/(app)/settings/components/settingsContentFrame.test.tsx
  • packages/web/src/app/(app)/settings/components/settingsContentFrame.tsx
  • packages/web/src/app/(app)/settings/layout.tsx
  • packages/web/src/app/(app)/settings/skills/page.tsx
  • packages/web/src/app/(app)/settings/skills/skillsEntitlementMessage.tsx
  • packages/web/src/app/(app)/settings/workspaceAskAgent/page.tsx
  • packages/web/src/app/(app)/settings/workspaceAskAgent/workspaceAskAgentPage.test.tsx
  • packages/web/src/app/(app)/settings/workspaceAskAgent/workspaceAskAgentPage.tsx
  • packages/web/src/app/api/(client)/client.ts
  • packages/web/src/app/api/(server)/ee/askmcp/configuration/[serverId]/tools/route.test.ts
  • packages/web/src/app/api/(server)/ee/chat/route.ts
  • packages/web/src/app/api/(server)/ee/skills/sourceStatus/route.ts
  • packages/web/src/components/ui/tooltip.tsx
  • packages/web/src/ee/features/chat/agent.ts
  • packages/web/src/ee/features/chat/components/chatThread/chatThread.tsx
  • packages/web/src/ee/features/chat/components/chatThread/detailsCard.test.tsx
  • packages/web/src/ee/features/chat/components/chatThread/detailsCard.tsx
  • packages/web/src/ee/features/chat/components/chatThread/tableOfContents.tsx
  • packages/web/src/ee/features/chat/components/chatThread/tools/loadSkillToolComponent.tsx
  • packages/web/src/ee/features/chat/components/chatThreadPanel.test.tsx
  • packages/web/src/ee/features/chat/components/chatThreadPanel.tsx
  • packages/web/src/ee/features/chat/mcp/components/accountAskAgentPage.test.tsx
  • packages/web/src/ee/features/chat/mcp/components/accountAskAgentPage.tsx
  • packages/web/src/ee/features/chat/mcp/components/connectorsMenu.tsx
  • packages/web/src/ee/features/chat/mcp/mcpToolSets.ts
  • packages/web/src/ee/features/chat/skills/actions.test.ts
  • packages/web/src/ee/features/chat/skills/actions.ts
  • packages/web/src/ee/features/chat/skills/commandCatalog.ts
  • packages/web/src/ee/features/chat/skills/commandResolution.test.ts
  • packages/web/src/ee/features/chat/skills/commandResolution.ts
  • packages/web/src/ee/features/chat/skills/commands.server.ts
  • packages/web/src/ee/features/chat/skills/components/importFromRepoDialog.test.tsx
  • packages/web/src/ee/features/chat/skills/components/importFromRepoDialog.tsx
  • packages/web/src/ee/features/chat/skills/components/personalSkillEditorPage.tsx
  • packages/web/src/ee/features/chat/skills/components/skillInstructionsEditor.test.ts
  • packages/web/src/ee/features/chat/skills/components/skillInstructionsEditor.tsx
  • packages/web/src/ee/features/chat/skills/components/skillsPage.test.tsx
  • packages/web/src/ee/features/chat/skills/components/skillsPage.tsx
  • packages/web/src/ee/features/chat/skills/components/workspaceSharedSkillsManager.test.tsx
  • packages/web/src/ee/features/chat/skills/components/workspaceSharedSkillsManager.tsx
  • packages/web/src/ee/features/chat/skills/components/workspaceSkillMutations.ts
  • packages/web/src/ee/features/chat/skills/components/workspaceSkillShared.tsx
  • packages/web/src/ee/features/chat/skills/loadSkillTool.test.ts
  • packages/web/src/ee/features/chat/skills/loadSkillTool.ts
  • packages/web/src/ee/features/chat/skills/registry.test.ts
  • packages/web/src/ee/features/chat/skills/registry.ts
  • packages/web/src/ee/features/chat/skills/skillAnalytics.test.ts
  • packages/web/src/ee/features/chat/skills/skillAnalytics.ts
  • packages/web/src/ee/features/chat/skills/sourceRepoAccess.test.ts
  • packages/web/src/ee/features/chat/skills/sourceRepoAccess.ts
  • packages/web/src/ee/features/chat/skills/types.test.ts
  • packages/web/src/ee/features/chat/skills/types.ts
  • packages/web/src/ee/features/chat/useTOCItems.ts
  • packages/web/src/ee/features/chat/useUnsavedChangesGuard.test.tsx
  • packages/web/src/ee/features/chat/useUnsavedChangesGuard.ts
  • packages/web/src/ee/features/oauth/server.ts
  • packages/web/src/features/chat/commands/types.ts
  • packages/web/src/features/chat/commands/utils.test.ts
  • packages/web/src/features/chat/commands/utils.ts
  • packages/web/src/features/chat/components/chatBox/chatBox.tsx
  • packages/web/src/features/chat/components/chatBox/keyboard.test.ts
  • packages/web/src/features/chat/components/chatBox/keyboard.ts
  • packages/web/src/features/chat/components/chatBox/sourceLabelBadge.tsx
  • packages/web/src/features/chat/components/chatBox/suggestionsBox.tsx
  • packages/web/src/features/chat/components/chatBox/types.ts
  • packages/web/src/features/chat/components/chatBox/useSuggestionModeAndQuery.ts
  • packages/web/src/features/chat/components/chatBox/useSuggestionsData.ts
  • packages/web/src/features/chat/mcpOAuthDraft.test.ts
  • packages/web/src/features/chat/mcpOAuthDraft.ts
  • packages/web/src/features/chat/types.ts
  • packages/web/src/features/chat/utils.test.ts
  • packages/web/src/features/chat/utils.ts
  • packages/web/src/features/git/getFileSourceApi.ts
  • packages/web/src/features/git/schemas.ts
  • packages/web/src/lib/errorCodes.ts
  • packages/web/src/lib/posthogEvents.ts
  • packages/web/src/lib/prismaErrors.ts
  • packages/web/tailwind.config.ts

Comment thread CHANGELOG.md
- [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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 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.

Suggested change
- [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

Comment on lines +773 to +780
// 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?

@coderabbitai coderabbitai Bot Jul 2, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ 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.

Suggested change
// 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@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:

  1. Skill is an org resource, ownership decoupled from a single user — switch createdBy/createdById to SetNull (matching updatedBy), and introduce an explicit notion of "current owner/maintainer" that's independent from the original author (e.g., allow any org OWNER to delete/manage a SHARED skill, 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 in actions.ts) plus making createdById nullable everywhere it's consumed (e.g., toSharedAgentSkillCatalogItem).
  2. Keep cascade, but guard the destructive path — leave the schema as-is, but before deleting a User, check for SHARED skills 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.
  3. 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 AgentSkillAdoption rows 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`.

Comment on lines +18 to +28
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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:


🏁 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/src

Repository: 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 || true

Repository: 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.

Comment on lines +181 to +187
// 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();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n "export const escapeMarkupMetacharacters" -A5 packages/web/src/lib/utils.ts

Repository: 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.ts

Repository: 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 -B4

Repository: 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

Comment on lines +579 to +609
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 });
}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔒 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.

Comment on lines +292 to +304
// 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,
}));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 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.ts

Repository: 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

Comment on lines +82 to +113
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ 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.tsx

Repository: 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.ts

Repository: 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 8

Repository: 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 6

Repository: 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.

Comment on lines +588 to +617
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);
}
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Comment on lines +228 to +238
<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",
)}
/>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Suggested change
<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.

Comment on lines +22 to +26
// 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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

could we compress into a single migration?

@brendan-kellam

Copy link
Copy Markdown
Contributor
image

nit: looks like the "x" button is not centered vertically

@brendan-kellam

Copy link
Copy Markdown
Contributor
image

Not something we need to do now, but it would be cool if the icon was clickable and it revealed a icon/emoji dropdown, similar to Linear:

image

// (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: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

are we sending posthog events for other instances (ex. create skill, edit skill, share skill, etc) @jsourcebot ? we should add events for every interaction

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants