Skip to content

feat: import skills/commands/agents from Git sources into sessions#1096

Open
Gkrumbach07 wants to merge 1 commit intomainfrom
feat/skill-import
Open

feat: import skills/commands/agents from Git sources into sessions#1096
Gkrumbach07 wants to merge 1 commit intomainfrom
feat/skill-import

Conversation

@Gkrumbach07
Copy link
Copy Markdown
Contributor

Summary

  • Add "Import Skills" option to the Add Context dropdown in session view
  • Clone any Git repo, discover skills/commands/agents, write them to file-uploads/.claude/
  • Claude Code auto-discovers imported skills via live change detection (no restart needed)
  • List and remove imported skills from the context panel

How it works

User clicks "Import Skills" → enters Git URL + branch + path
    ↓
Backend clones repo, scans for .claude/skills/, .claude/commands/, .claude/agents/
(also scans skills/, commands/, agents/ for ai-helpers layout)
    ↓
Writes each file via runner's existing /content/write to file-uploads/.claude/
    ↓
Claude auto-discovers new skills (live change detection on add_dirs)
    ↓
Skills appear in context tab with type badges + remove buttons

Changes

Component Change
Backend handlers ImportSkillSource, ListImportedSkills, RemoveImportedSkill
Backend routes POST/GET/DELETE under /agentic-sessions/:sn/skills/
Frontend modal New ImportSkillsModal with Git URL/branch/path form
Frontend context tab "Skills" section with type badges and remove buttons
Frontend proxy routes 3 new Next.js API route handlers

Design decisions

  • No CRD changes — skills stored in file-uploads/.claude/, persisted via S3 state-sync
  • No new runner endpoints — reuses existing /content/write and /content/delete
  • Dual-pattern scanning — supports both .claude/{type}/ and direct {type}/ layouts
  • Deduplication.claude/ pattern takes precedence when both exist

Test plan

  • Start session, click Add → Import Skills
  • Enter https://github.com/opendatahub-io/ai-helpers.git, branch main, path helpers
  • Verify toast shows imported count
  • Verify skills appear in context tab with badges
  • Type / in chat → imported skills appear in autocomplete
  • Remove a skill → disappears from context and autocomplete
  • Resume session → skills persist (S3 state-sync)

🤖 Generated with Claude Code

Add the ability to import skills, commands, and agents from any Git
repository into a running session. Files are written to the session's
file-uploads/.claude/ directory, which Claude Code auto-discovers via
live change detection — no session restart needed.

Backend:
- ImportSkillSource handler: clones repo, scans for skills/commands/agents
  in both .claude/ and root-level patterns (ai-helpers layout), writes
  each file via runner's existing /content/write endpoint
- ListImportedSkills handler: lists imported items from file-uploads/.claude/
- RemoveImportedSkill handler: deletes items via /content/delete
- Dual-pattern scanning with deduplication

Frontend:
- "Import Skills" option in Add Context dropdown with Git URL/branch/path form
- Skills section in context tab showing imported items with type badges
- Remove button per item
- Next.js proxy routes for all three endpoints

No CRD changes. No new runner endpoints. Reuses existing file upload
infrastructure. Skills persist across session resume via S3 state-sync.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 30, 2026

Walkthrough

This pull request introduces a complete skill/command/agent import and management system. The backend adds three handlers that validate requests, clone Git repositories, parse metadata, write content to a runner, and manage removals. The frontend exposes three API routes forwarding to these handlers, introduces a skills display panel in the context tab with React Query, and adds a modal dialog for importing skills from Git repositories.

Changes

Cohort / File(s) Summary
Backend Skill Management
components/backend/handlers/sessions.go
Added 609 lines implementing three Gin handlers: ImportSkillSource (clones Git repo, parses skills/commands/agents, writes to runner), ListImportedSkills (queries runner directories and fetches markdown frontmatter), and RemoveImportedSkill (deletes runner paths). Includes helper functions for YAML frontmatter parsing, file I/O to runner, and Git URL validation.
Backend Route Registration
components/backend/routes.go
Added three new project-scoped routes under /api/projects/:projectName/agentic-sessions/:sessionName/skills: POST /import, GET /, and DELETE /:type/:skillId.
Frontend API Layer
components/frontend/src/app/api/projects/[name]/agentic-sessions/[sessionName]/skills/route.ts, components/frontend/src/app/api/projects/[name]/agentic-sessions/[sessionName]/skills/import/route.ts, components/frontend/src/app/api/projects/[name]/agentic-sessions/[sessionName]/skills/[type]/[skillId]/route.ts
Added three route handlers forwarding GET (list), POST (import), and DELETE (remove) requests to the backend with proper parameter encoding and header forwarding.
Frontend Skills UI Display
components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/context-tab.tsx, components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/explorer-panel.tsx
Added skills list rendering with React Query data fetching, per-item removal with confirmation, and badge styling by skill type. ContextTab now accepts onImportSkills, projectName, and sessionName props; ExplorerPanel forwards these props along with the callback.
Frontend Skills Import Modal
components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/modals/import-skills-modal.tsx, components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx
Added ImportSkillsModal component with Git URL/branch/path form inputs, and integrated import mutation on the session detail page. On success, invalidates skills cache and shows toast feedback.
Frontend Test Updates
components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/__tests__/context-tab.test.tsx, components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/__tests__/explorer-panel.test.tsx
Added required props (onImportSkills, projectName, sessionName) to test fixtures.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Frontend
    participant FrontendAPI as Frontend API Layer
    participant Backend
    participant Git
    participant Runner

    User->>Frontend: Open import modal, enter Git URL
    User->>Frontend: Submit import form
    Frontend->>FrontendAPI: POST /skills/import (url, branch, path)
    FrontendAPI->>Backend: POST /skills/import
    Backend->>Git: Git clone repo to temp dir
    Git-->>Backend: Repo files
    Backend->>Backend: Scan for skills/commands/agents
    Backend->>Backend: Parse YAML frontmatter metadata
    Backend->>Runner: POST /content/write (for each skill file)
    Runner-->>Backend: File written
    Backend-->>FrontendAPI: { count, imported: [...] }
    FrontendAPI-->>Frontend: { count, imported: [...] }
    Frontend->>Frontend: Invalidate skills query cache
    Frontend->>FrontendAPI: GET /skills (refetch)
    FrontendAPI->>Backend: GET /skills
    Backend->>Runner: GET directory listings + file contents
    Runner-->>Backend: Skills metadata
    Backend-->>FrontendAPI: { items: [...], count }
    FrontendAPI-->>Frontend: { items: [...], count }
    Frontend->>User: Show success toast, display imported skills
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly and accurately describes the main change: adding the ability to import skills/commands/agents from Git sources into sessions, which is the core feature of this PR.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, providing a clear summary of functionality, design decisions, and test plan that align with the implemented changes.

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

✨ 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 feat/skill-import

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

req.Header.Set("Content-Type", "application/json")

client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)

Check failure

Code scanning / CodeQL

Uncontrolled data used in network request Critical

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
req.Header.Set("Content-Type", "application/json")

client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)

Check failure

Code scanning / CodeQL

Uncontrolled data used in network request Critical

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
}
defer os.RemoveAll(tmpDir)

cmd := exec.CommandContext(c.Request.Context(), "git", "clone", "--depth", "1", "-b", req.Branch, req.URL, tmpDir)

Check failure

Code scanning / CodeQL

Command built from user-controlled sources Critical

This command depends on a
user-provided value
.
continue
}

resp, err := client.Do(req)

Check failure

Code scanning / CodeQL

Uncontrolled data used in network request Critical

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
mdURL := fmt.Sprintf("%s/content/file?path=%s", runnerBase, url.QueryEscape(skillMDPath))
mdReq, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, mdURL, nil)
if err == nil {
mdResp, err := client.Do(mdReq)

Check failure

Code scanning / CodeQL

Uncontrolled data used in network request Critical

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
fileURL := fmt.Sprintf("%s/content/file?path=%s", runnerBase, url.QueryEscape(filePath))
fileReq, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, fileURL, nil)
if err == nil {
fileResp, err := client.Do(fileReq)

Check failure

Code scanning / CodeQL

Uncontrolled data used in network request Critical

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
return
}
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)

Check failure

Code scanning / CodeQL

Uncontrolled data used in network request Critical

The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
The
URL
of this request depends on a
user-provided value
.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/backend/handlers/sessions.go`:
- Around line 4610-4614: The validation for itemType only accepts plural values
("skills","commands","agents") which rejects frontend requests that send
singular values; update the check around the itemType variable (the validation
block in sessions.go) to accept both singular and plural forms (e.g., allow
"skill" and "skills", "command" and "commands", "agent" and "agents") or
normalize itemType by mapping singular -> plural before validation/usage, and
ensure any downstream code that relies on the plural form uses the normalized
value so removals like `/skills/skill/<id>` succeed.
- Around line 4679-4690: The current loop in RemoveImportedSkill iterates
listResp.Items and skips directories, leaving empty skill directories behind;
update the cleanup to remove directories after their files (or perform a
recursive delete) by calling deletePathFromRunner for the directory path as
well—identify the skill directory from f.Path (or derive it from the skill base
path used in the loop) and invoke deletePathFromRunner(c.Request.Context(),
runnerBase, relDir) after files are deleted (or replace the per-file logic with
a single recursive delete call), and preserve the existing error logging pattern
(the log.Printf line) for any delete failures.
- Around line 4300-4305: The loop that scans for skills uses prefixes
[]string{".claude", ""} causing root-level skills to overwrite .claude versions;
flip the scan order to process "" after ".claude" so .claude takes precedence,
or implement a de-duplication map (e.g., imported map[string]skillItem keyed by
type-id like fmt.Sprintf("%s-%s", "skill", skillName)) in the import loop to
skip adding an already-imported skill; update the code around variables
scanRoot, prefix, skillsDir and the import logic that constructs skill items to
consult the imported map before appending.

In
`@components/frontend/src/app/projects/`[name]/sessions/[sessionName]/components/explorer/context-tab.tsx:
- Line 57: Tests fail because the component now calls useQueryClient(), so wrap
test renders in a QueryClientProvider: in the test file create a QueryClient
instance (with defaultOptions.queries.retry = false), add a renderWithProviders
helper that returns render(<QueryClientProvider
client={queryClient}>{ui}</QueryClientProvider>), and replace render(...) calls
for the ContextTab component with renderWithProviders(...); ensure imports for
QueryClient and QueryClientProvider from "@tanstack/react-query" are added.

In `@components/frontend/src/app/projects/`[name]/sessions/[sessionName]/page.tsx:
- Around line 582-604: The mutation currently throws a generic Error("Import
failed") inside importSkillsMutation -> mutationFn and discards backend details;
update mutationFn to read the response body when response.ok is false (try
response.json() then fallback to response.text()) and throw a new Error that
includes the HTTP status and the backend error message, and keep onError as is
so toast.error(error.message || "Failed to import skills") will display the
backend-provided details; locate the fetch in importSkillsMutation -> mutationFn
and replace the generic throw with parsing the response body and constructing a
descriptive Error that includes response.status and the parsed error message.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: ef860f29-b0b1-4099-ae45-4274059c4f05

📥 Commits

Reviewing files that changed from the base of the PR and between 8ac0f85 and 620f7fc.

📒 Files selected for processing (11)
  • components/backend/handlers/sessions.go
  • components/backend/routes.go
  • components/frontend/src/app/api/projects/[name]/agentic-sessions/[sessionName]/skills/[type]/[skillId]/route.ts
  • components/frontend/src/app/api/projects/[name]/agentic-sessions/[sessionName]/skills/import/route.ts
  • components/frontend/src/app/api/projects/[name]/agentic-sessions/[sessionName]/skills/route.ts
  • components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/__tests__/context-tab.test.tsx
  • components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/__tests__/explorer-panel.test.tsx
  • components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/context-tab.tsx
  • components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/explorer-panel.tsx
  • components/frontend/src/app/projects/[name]/sessions/[sessionName]/components/modals/import-skills-modal.tsx
  • components/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx

Comment on lines +4300 to +4305
// Scan for skills: both .claude/skills/*/SKILL.md and skills/*/SKILL.md
for _, prefix := range []string{".claude", ""} {
skillsDir := filepath.Join(scanRoot, prefix, "skills")
entries, err := os.ReadDir(skillsDir)
if err != nil {
continue
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.

⚠️ Potential issue | 🟠 Major

Dual-pattern scanning precedence is inverted.

The PR description states ".claude/ takes precedence when both exist", but the current implementation processes .claude first, then root-level. Since files are written to the same destination path, the root-level version overwrites the .claude version, giving root-level precedence instead.

🐛 Proposed fix: Reverse loop order or track processed items

Option 1: Reverse loop order (simpler)

-for _, prefix := range []string{".claude", ""} {
+for _, prefix := range []string{"", ".claude"} {

Option 2: Track already-imported items (prevents duplicates in response)

imported := make(map[string]skillItem) // key: type-id

// In the import loop, check before adding:
key := fmt.Sprintf("%s-%s", "skill", skillName)
if _, exists := imported[key]; exists {
    continue // Already imported from .claude/ path
}
imported[key] = skillItem{...}
📝 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
// Scan for skills: both .claude/skills/*/SKILL.md and skills/*/SKILL.md
for _, prefix := range []string{".claude", ""} {
skillsDir := filepath.Join(scanRoot, prefix, "skills")
entries, err := os.ReadDir(skillsDir)
if err != nil {
continue
// Scan for skills: both .claude/skills/*/SKILL.md and skills/*/SKILL.md
for _, prefix := range []string{"", ".claude"} {
skillsDir := filepath.Join(scanRoot, prefix, "skills")
entries, err := os.ReadDir(skillsDir)
if err != nil {
continue
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/sessions.go` around lines 4300 - 4305, The loop
that scans for skills uses prefixes []string{".claude", ""} causing root-level
skills to overwrite .claude versions; flip the scan order to process "" after
".claude" so .claude takes precedence, or implement a de-duplication map (e.g.,
imported map[string]skillItem keyed by type-id like fmt.Sprintf("%s-%s",
"skill", skillName)) in the import loop to skip adding an already-imported
skill; update the code around variables scanRoot, prefix, skillsDir and the
import logic that constructs skill items to consult the imported map before
appending.

Comment on lines +4610 to +4614
// Validate type
if itemType != "skills" && itemType != "commands" && itemType != "agents" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Type must be 'skills', 'commands', or 'agents'"})
return
}
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.

⚠️ Potential issue | 🔴 Critical

Type parameter mismatch will cause 400 errors on skill removal.

The backend expects plural type values ("skills", "commands", "agents"), but the frontend sends singular values ("skill", "command", "agent") based on SkillItem.type.

When a user tries to remove a skill, the request URL will be /skills/skill/my-skill-id and the validation will fail.

🐛 Proposed fix: Accept both singular and plural forms
 // Validate type
-if itemType != "skills" && itemType != "commands" && itemType != "agents" {
-    c.JSON(http.StatusBadRequest, gin.H{"error": "Type must be 'skills', 'commands', or 'agents'"})
+// Accept both singular and plural forms for flexibility
+typeMap := map[string]string{
+    "skill": "skills", "skills": "skills",
+    "command": "commands", "commands": "commands",
+    "agent": "agents", "agents": "agents",
+}
+normalizedType, ok := typeMap[itemType]
+if !ok {
+    c.JSON(http.StatusBadRequest, gin.H{"error": "Type must be 'skill', 'command', or 'agent'"})
     return
 }
+itemType = normalizedType

Alternatively, update the frontend mutation to use plural forms:

// In context-tab.tsx removeSkillMutation
`/api/projects/${projectName}/agentic-sessions/${sessionName}/skills/${type}s/${id}`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/sessions.go` around lines 4610 - 4614, The
validation for itemType only accepts plural values
("skills","commands","agents") which rejects frontend requests that send
singular values; update the check around the itemType variable (the validation
block in sessions.go) to accept both singular and plural forms (e.g., allow
"skill" and "skills", "command" and "commands", "agent" and "agents") or
normalize itemType by mapping singular -> plural before validation/usage, and
ensure any downstream code that relies on the plural form uses the normalized
value so removals like `/skills/skill/<id>` succeed.

Comment on lines +4679 to +4690
// Delete each file in the skill directory
for _, f := range listResp.Items {
if f.IsDir {
continue
}
// Path from runner is absolute like /file-uploads/.claude/skills/foo/SKILL.md
// content/delete expects relative path
relPath := strings.TrimPrefix(f.Path, "/")
if err := deletePathFromRunner(c.Request.Context(), runnerBase, relPath); err != nil {
log.Printf("RemoveImportedSkill: failed to delete %s: %v", relPath, err)
}
}
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.

⚠️ Potential issue | 🟡 Minor

Skill directory cleanup is incomplete.

The deletion logic skips subdirectories (line 4681-4683) and doesn't remove the skill directory itself after deleting files. This leaves empty directories in file-uploads/.claude/skills/. While not functionally breaking (the skill won't appear in listings), it creates orphaned directories over time.

Consider using a recursive delete approach or documenting this as expected behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/backend/handlers/sessions.go` around lines 4679 - 4690, The
current loop in RemoveImportedSkill iterates listResp.Items and skips
directories, leaving empty skill directories behind; update the cleanup to
remove directories after their files (or perform a recursive delete) by calling
deletePathFromRunner for the directory path as well—identify the skill directory
from f.Path (or derive it from the skill base path used in the loop) and invoke
deletePathFromRunner(c.Request.Context(), runnerBase, relDir) after files are
deleted (or replace the per-file logic with a single recursive delete call), and
preserve the existing error logging pattern (the log.Printf line) for any delete
failures.

const [removingFile, setRemovingFile] = useState<string | null>(null);
const [removingSkill, setRemovingSkill] = useState<string | null>(null);
const [expandedRepos, setExpandedRepos] = useState<Set<string>>(new Set());
const queryClient = useQueryClient();
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.

⚠️ Potential issue | 🔴 Critical

Unit tests failing: Component now requires QueryClientProvider wrapper.

The addition of useQueryClient() requires all tests rendering this component to be wrapped in a QueryClientProvider. The static analysis shows all 8 tests are failing with "No QueryClient set, use QueryClientProvider to set one".

Update the test file to wrap the component:

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient({
  defaultOptions: { queries: { retry: false } },
});

const renderWithProviders = (ui: React.ReactElement) => {
  return render(
    <QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
  );
};

// Then use renderWithProviders(<ContextTab ... />) instead of render(...)
🧰 Tools
🪛 GitHub Check: Frontend Unit Tests (Vitest)

[failure] 57-57: src/app/projects/[name]/sessions/[sessionName]/components/explorer/tests/context-tab.test.tsx > ContextTab > shows Add and Upload buttons when canModify is true
Error: No QueryClient set, use QueryClientProvider to set one
❯ useQueryClient node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js:15:11
❯ ContextTab src/app/projects/[name]/sessions/[sessionName]/components/explorer/context-tab.tsx:57:23
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:17450:11


[failure] 57-57: src/app/projects/[name]/sessions/[sessionName]/components/explorer/tests/context-tab.test.tsx > ContextTab > hides Upload File button when canModify is false
Error: No QueryClient set, use QueryClientProvider to set one
❯ useQueryClient node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js:15:11
❯ ContextTab src/app/projects/[name]/sessions/[sessionName]/components/explorer/context-tab.tsx:57:23
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:17450:11


[failure] 57-57: src/app/projects/[name]/sessions/[sessionName]/components/explorer/tests/context-tab.test.tsx > ContextTab > hides Add Repository button when canModify is false
Error: No QueryClient set, use QueryClientProvider to set one
❯ useQueryClient node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js:15:11
❯ ContextTab src/app/projects/[name]/sessions/[sessionName]/components/explorer/context-tab.tsx:57:23
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:17450:11


[failure] 57-57: src/app/projects/[name]/sessions/[sessionName]/components/explorer/tests/context-tab.test.tsx > ContextTab > shows repo branch badge
Error: No QueryClient set, use QueryClientProvider to set one
❯ useQueryClient node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js:15:11
❯ ContextTab src/app/projects/[name]/sessions/[sessionName]/components/explorer/context-tab.tsx:57:23
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:17450:11


[failure] 57-57: src/app/projects/[name]/sessions/[sessionName]/components/explorer/tests/context-tab.test.tsx > ContextTab > renders uploaded file items
Error: No QueryClient set, use QueryClientProvider to set one
❯ useQueryClient node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js:15:11
❯ ContextTab src/app/projects/[name]/sessions/[sessionName]/components/explorer/context-tab.tsx:57:23
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:17450:11


[failure] 57-57: src/app/projects/[name]/sessions/[sessionName]/components/explorer/tests/context-tab.test.tsx > ContextTab > renders repository items
Error: No QueryClient set, use QueryClientProvider to set one
❯ useQueryClient node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js:15:11
❯ ContextTab src/app/projects/[name]/sessions/[sessionName]/components/explorer/context-tab.tsx:57:23
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:17450:11


[failure] 57-57: src/app/projects/[name]/sessions/[sessionName]/components/explorer/tests/context-tab.test.tsx > ContextTab > renders Add button in header
Error: No QueryClient set, use QueryClientProvider to set one
❯ useQueryClient node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js:15:11
❯ ContextTab src/app/projects/[name]/sessions/[sessionName]/components/explorer/context-tab.tsx:57:23
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:17450:11


[failure] 57-57: src/app/projects/[name]/sessions/[sessionName]/components/explorer/tests/context-tab.test.tsx > ContextTab > renders empty state when no repos or files
Error: No QueryClient set, use QueryClientProvider to set one
❯ useQueryClient node_modules/@tanstack/react-query/build/modern/QueryClientProvider.js:15:11
❯ ContextTab src/app/projects/[name]/sessions/[sessionName]/components/explorer/context-tab.tsx:57:23
❯ Object.react_stack_bottom_frame node_modules/react-dom/cjs/react-dom-client.development.js:25904:20
❯ renderWithHooks node_modules/react-dom/cjs/react-dom-client.development.js:7662:22
❯ updateFunctionComponent node_modules/react-dom/cjs/react-dom-client.development.js:10166:19
❯ beginWork node_modules/react-dom/cjs/react-dom-client.development.js:11778:18
❯ runWithFiberInDEV node_modules/react-dom/cjs/react-dom-client.development.js:874:13
❯ performUnitOfWork node_modules/react-dom/cjs/react-dom-client.development.js:17641:22
❯ workLoopSync node_modules/react-dom/cjs/react-dom-client.development.js:17469:41
❯ renderRootSync node_modules/react-dom/cjs/react-dom-client.development.js:17450:11

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/frontend/src/app/projects/`[name]/sessions/[sessionName]/components/explorer/context-tab.tsx
at line 57, Tests fail because the component now calls useQueryClient(), so wrap
test renders in a QueryClientProvider: in the test file create a QueryClient
instance (with defaultOptions.queries.retry = false), add a renderWithProviders
helper that returns render(<QueryClientProvider
client={queryClient}>{ui}</QueryClientProvider>), and replace render(...) calls
for the ContextTab component with renderWithProviders(...); ensure imports for
QueryClient and QueryClientProvider from "@tanstack/react-query" are added.

Comment on lines +582 to +604
// Import skills mutation
const importSkillsMutation = useMutation({
mutationFn: async (source: { url: string; branch: string; path?: string }) => {
const response = await fetch(
`/api/projects/${projectName}/agentic-sessions/${sessionName}/skills/import`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(source),
}
);
if (!response.ok) throw new Error("Import failed");
return response.json() as Promise<{ count: number }>;
},
onSuccess: (data) => {
toast.success(`Imported ${data.count} items`);
queryClient.invalidateQueries({ queryKey: ["skills", projectName, sessionName] });
setImportSkillsModalOpen(false);
},
onError: (error: Error) => {
toast.error(error.message || "Failed to import skills");
},
});
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.

⚠️ Potential issue | 🟡 Minor

Backend error details are discarded on failure.

When the import fails, the mutation throws a generic "Import failed" message, losing any diagnostic information from the backend (e.g., invalid URL, path not found, clone failure). This degrades the user experience when troubleshooting.

🛠️ Proposed fix to preserve backend error details
   const importSkillsMutation = useMutation({
     mutationFn: async (source: { url: string; branch: string; path?: string }) => {
       const response = await fetch(
         `/api/projects/${projectName}/agentic-sessions/${sessionName}/skills/import`,
         {
           method: "POST",
           headers: { "Content-Type": "application/json" },
           body: JSON.stringify(source),
         }
       );
-      if (!response.ok) throw new Error("Import failed");
+      if (!response.ok) {
+        const errorData = await response.json().catch(() => ({}));
+        throw new Error(errorData.error || "Import failed");
+      }
       return response.json() as Promise<{ count: number }>;
     },
📝 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
// Import skills mutation
const importSkillsMutation = useMutation({
mutationFn: async (source: { url: string; branch: string; path?: string }) => {
const response = await fetch(
`/api/projects/${projectName}/agentic-sessions/${sessionName}/skills/import`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(source),
}
);
if (!response.ok) throw new Error("Import failed");
return response.json() as Promise<{ count: number }>;
},
onSuccess: (data) => {
toast.success(`Imported ${data.count} items`);
queryClient.invalidateQueries({ queryKey: ["skills", projectName, sessionName] });
setImportSkillsModalOpen(false);
},
onError: (error: Error) => {
toast.error(error.message || "Failed to import skills");
},
});
// Import skills mutation
const importSkillsMutation = useMutation({
mutationFn: async (source: { url: string; branch: string; path?: string }) => {
const response = await fetch(
`/api/projects/${projectName}/agentic-sessions/${sessionName}/skills/import`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(source),
}
);
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.error || "Import failed");
}
return response.json() as Promise<{ count: number }>;
},
onSuccess: (data) => {
toast.success(`Imported ${data.count} items`);
queryClient.invalidateQueries({ queryKey: ["skills", projectName, sessionName] });
setImportSkillsModalOpen(false);
},
onError: (error: Error) => {
toast.error(error.message || "Failed to import skills");
},
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/frontend/src/app/projects/`[name]/sessions/[sessionName]/page.tsx
around lines 582 - 604, The mutation currently throws a generic Error("Import
failed") inside importSkillsMutation -> mutationFn and discards backend details;
update mutationFn to read the response body when response.ok is false (try
response.json() then fallback to response.text()) and throw a new Error that
includes the HTTP status and the backend error message, and keep onError as is
so toast.error(error.message || "Failed to import skills") will display the
backend-provided details; locate the fetch in importSkillsMutation -> mutationFn
and replace the generic throw with parsing the response body and constructing a
descriptive Error that includes response.status and the parsed error message.

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.

1 participant