feat: import skills/commands/agents from Git sources into sessions#1096
feat: import skills/commands/agents from Git sources into sessions#1096Gkrumbach07 wants to merge 1 commit intomainfrom
Conversation
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.
WalkthroughThis 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
| 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
| 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
| } | ||
| 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
| continue | ||
| } | ||
|
|
||
| resp, err := client.Do(req) |
Check failure
Code scanning / CodeQL
Uncontrolled data used in network request Critical
| 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
| 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
| 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
There was a problem hiding this comment.
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
📒 Files selected for processing (11)
components/backend/handlers/sessions.gocomponents/backend/routes.gocomponents/frontend/src/app/api/projects/[name]/agentic-sessions/[sessionName]/skills/[type]/[skillId]/route.tscomponents/frontend/src/app/api/projects/[name]/agentic-sessions/[sessionName]/skills/import/route.tscomponents/frontend/src/app/api/projects/[name]/agentic-sessions/[sessionName]/skills/route.tscomponents/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/__tests__/context-tab.test.tsxcomponents/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/__tests__/explorer-panel.test.tsxcomponents/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/context-tab.tsxcomponents/frontend/src/app/projects/[name]/sessions/[sessionName]/components/explorer/explorer-panel.tsxcomponents/frontend/src/app/projects/[name]/sessions/[sessionName]/components/modals/import-skills-modal.tsxcomponents/frontend/src/app/projects/[name]/sessions/[sessionName]/page.tsx
| // 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 |
There was a problem hiding this comment.
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.
| // 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.
| // Validate type | ||
| if itemType != "skills" && itemType != "commands" && itemType != "agents" { | ||
| c.JSON(http.StatusBadRequest, gin.H{"error": "Type must be 'skills', 'commands', or 'agents'"}) | ||
| return | ||
| } |
There was a problem hiding this comment.
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 = normalizedTypeAlternatively, 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.
| // 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
| // 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"); | ||
| }, | ||
| }); |
There was a problem hiding this comment.
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.
| // 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.
Summary
file-uploads/.claude/How it works
Changes
ImportSkillSource,ListImportedSkills,RemoveImportedSkill/agentic-sessions/:sn/skills/ImportSkillsModalwith Git URL/branch/path formDesign decisions
file-uploads/.claude/, persisted via S3 state-sync/content/writeand/content/delete.claude/{type}/and direct{type}/layouts.claude/pattern takes precedence when both existTest plan
https://github.com/opendatahub-io/ai-helpers.git, branchmain, pathhelpers/in chat → imported skills appear in autocomplete🤖 Generated with Claude Code