Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ EntityConfig (설정 객체) 공통 UI Primitives 도메
│ type, icon, actions │────→│ EntityListPanel │ │ SkillInspector │
│ getKey, getLabel │ │ EntityInspector │ │ McpInspector │
│ groupBy?, trailing? │ │ Inspector primitives │ │ HookInspector │
toDetailTarget │ │ (shadcn 스타일) │ │ ... │
│ │ (shadcn 스타일) │ │ ... │
└──────────────────────┘ └───────────────────────┘ └───────────────────┘
```

Expand All @@ -51,7 +51,7 @@ EntityConfig (설정 객체) 공통 UI Primitives 도메
|---------|------|------|
| `EntityConfig<T>` | 엔티티별 설정 객체 (아이콘, 액션, 라벨, 그룹화) — 리스트 전용 | `src/config/entity-registry.ts` |
| `EntityListPanel` | 제네릭 리스트 렌더러 — flat list + groupBy 지원 | `src/components/board/EntityListPanel.tsx` |
| `EntityInspector` | 타입별 라우터 — target.type → 도메인 Inspector | `src/components/board/entity-inspector.tsx` |
| `EntityInspector` | 타입별 라우터 — `{ type, itemKey }` → 도메인 Inspector | `src/components/board/entity-inspector.tsx` |
| `Inspector` primitives | shadcn 스타일 compound components (Inspector, InspectorHeader, InspectorBody 등) | `src/components/ui/inspector.tsx` |
| `BoardLayout` | Notion 스타일 칸반 보드 — 스코프 행, Sheet 상세 | `src/components/board/BoardLayout.tsx` |

Expand Down Expand Up @@ -180,7 +180,6 @@ interface EntityConfig<T> {
getScope?: (item: T) => string | undefined;
groupBy?: (item: T) => string;
trailing?: (item: T) => ReactNode;
toDetailTarget: (item: T) => DashboardDetailTarget;
}
```

Expand Down
16 changes: 16 additions & 0 deletions docs/CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@
- Record/object를 직접 접근할 수 있으면 헬퍼 만들지 말 것 (`agents[name].displayName` > `getAgentDisplayName(name)`)
- 인덱스 파일(`index.ts`)로 래퍼만 만드는 패턴 지양

## Hook Key 규칙

Hook의 고유 키는 `hookConfig.getKey()`가 정의하는 정규 형식을 따른다:

```
${scope}:${event}:${matcher ?? ""}:${command ?? prompt ?? ""}
```

- `scope`: `"user"` | `"project"`
- `event`: Hook 이벤트명 (`PreToolUse`, `SessionStart` 등)
- `matcher`: 정규식 문자열 (없으면 빈 문자열). Claude Code 공식 스키마에서 matcher group의 필터
- `command`/`prompt`: 개별 hook handler 식별자

패널에서 key를 생성할 때 반드시 group(matcher)을 순회하여 정규 형식과 일치시킬 것.
`allHooks` 같은 flat 배열로 순회하면 matcher 정보가 유실된다.

## Claude CLI 서버 호출 (서버 사이드)

- `execFile` 대신 `spawn` + `stdio: ['ignore', 'pipe', 'pipe']` 사용 — Nitro dev 서버는 TTY 없음, `execFile`로 `claude` CLI 호출 시 stdin 대기로 hanging 발생
Expand Down
45 changes: 36 additions & 9 deletions docs/DESIGN-SYSTEM.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ BoardLayout
| 리스트 행 | `ListItem` / `ListSubItem` | `src/components/ui/list-item.tsx` |
| 행 내부 구성 | `Item`, `ItemMedia`, `ItemContent`, `ItemTitle`, `ItemActions` | `src/components/ui/item.tsx` |
| 빈 상태 | `Empty`, `EmptyMedia`, `EmptyDescription` | `src/components/ui/empty.tsx` |
| Inspector | `Inspector`, `InspectorHeader`, `InspectorTitle`, `InspectorBody` | `src/components/ui/inspector.tsx` |
| Inspector | `Inspector`, `InspectorHeader`, `InspectorTitle`, `InspectorActions`, `InspectorBody`, `InspectorSkeleton` | `src/components/ui/inspector.tsx` |
| 우클릭 메뉴 | `ItemContextMenu` / `SubItemContextMenu` | `src/components/ui/item-context-menu.tsx` |

패널 리스트 행은 항상 `size="xs"` 고정.
Expand Down Expand Up @@ -68,8 +68,7 @@ lucide-react에서 엔티티 아이콘을 직접 import하면 사이드바와
```tsx
interface XxxPanelProps {
scopeFilter?: string
onSelectItem?: (target: DashboardDetailTarget) => void
onAction?: (id: EntityActionId, target: NonNullable<DashboardDetailTarget>) => void
onSelectItem?: (selected: { type: string; key: string }) => void
}
```

Expand Down Expand Up @@ -194,12 +193,40 @@ export function XxxPanel({ scopeFilter, onSelectItem, onAction }: XxxPanelProps)

## 새 엔티티 추가 시 업데이트 대상

1. `src/components/board/types.ts` — `DashboardDetailTarget` 유니온
2. `src/components/board/BoardLayout.tsx` — `allColumnDefs` + `renderPanel()`
3. `src/components/board/entity-inspector.tsx` — Inspector 라우팅
4. `src/components/icons/entity-icons.tsx` — 아이콘 추가
5. `messages/en/common.json` + `messages/ko/common.json` — `board_no_*` 메시지
6. `src/config/entities/` — 엔티티 config 파일
1. `src/components/board/BoardLayout.tsx` — `allColumnDefs` + `renderPanel()`
2. `src/components/board/entity-inspector.tsx` — Inspector 라우팅
3. `src/components/icons/entity-icons.tsx` — 아이콘 추가
4. `messages/en/common.json` + `messages/ko/common.json` — `board_no_*` 메시지
5. `src/config/entities/` — 엔티티 config 파일

## Inspector 패턴

각 Inspector는 `itemKey: string` prop만 받고, 내부에서 React Query 훅으로 데이터를 직접 fetch한다.
액션 처리(delete, open-in-editor)도 Inspector 내부에서 담당한다.

```tsx
export function XxxInspector({ itemKey }: { itemKey: string }) {
const { data: items = [] } = useXxxQuery()
const item = items.find((i) => xxxConfig.getKey(i) === itemKey)

if (!item) return <InspectorSkeleton />

return (
<Inspector>
<InspectorHeader>
<InspectorTitle icon={ENTITY_ICONS.xxx} title={item.name} />
<InspectorActions>
<EntityActionDropdown actions={ENTITY_ACTIONS.xxx} onAction={handleAction} />
</InspectorActions>
</InspectorHeader>
<InspectorBody>...</InspectorBody>
</Inspector>
)
}
```

- `InspectorSkeleton` — 아이템 로딩 중 표시. 아이템을 찾을 수 없는 경우에도 사용
- `entity-inspector.tsx` — `type`에 따라 올바른 Inspector 컴포넌트로 라우팅

## 참조 구현

Expand Down
1 change: 1 addition & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ Claude Code가 자체 설정 관리 GUI를 추가하면, 그걸 쓰면 된다. a

## Shipped

- **Inspector 아키텍처 단순화** (2026-03-18) — `DashboardDetailTarget` union / `toDetailTarget` 제거. Inspector가 `itemKey`만 받아 자체 fetch 및 액션 처리. `use-entity-action-handler.ts` 제거.
- **Inspector 리디자인** (2026-03-18) — DetailPanel/EntityDetailPanel/DetailContent 시스템을 Inspector primitives + 도메인별 full Inspector로 전환. entity/ 해소, 도메인 디렉토리 이동.
- **Commands in Skills 패널** (2026-03-18) — `.claude/commands/` 파일을 Skills 열에 통합 표시. `commandConfig` + `ENTITY_ICONS.command`(TerminalSquareIcon) 추가, namespace 트리 그룹핑(`GroupedList`), 디테일 패널 slash command 필드 + 복사 버튼, `getSlashCommand` 유틸.
- **마켓플레이스 UI 리디자인** (2026-03-15) — skills.sh 연동 마켓플레이스 전면 재설계. SkillDetailView 통합, provider별 SKILL.md fetch, 글로벌+프로젝트 lock 합산 conflict 감지.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ohing504/agentfiles",
"type": "module",
"packageManager": "pnpm@10.30.3",
"packageManager": "pnpm@10.32.1",
"version": "0.3.0",
"description": "Discover, understand, and manage your AI agent workflows",
"author": "Youngsup Oh",
Expand Down
87 changes: 69 additions & 18 deletions src/components/agent/agent-inspector.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,101 @@
import { useQueryClient } from "@tanstack/react-query"
import { toast } from "sonner"
import { DetailField } from "@/components/DetailField"
import { FileViewer } from "@/components/FileViewer"
import { ENTITY_ICONS } from "@/components/icons/entity-icons"
import { useProjectContext } from "@/components/ProjectContext"
import { EntityActionDropdown } from "@/components/ui/entity-action-menu"
import {
Inspector,
InspectorActions,
InspectorBody,
InspectorHeader,
InspectorSkeleton,
InspectorTitle,
} from "@/components/ui/inspector"
import { Separator } from "@/components/ui/separator"
import { useAgentFileDetailQuery } from "@/hooks/use-agent-file-detail"
import { useAgentFiles } from "@/hooks/use-config"
import { usePluginsQuery } from "@/hooks/use-plugins"
import { ENTITY_ACTIONS, type EntityActionId } from "@/lib/entity-actions"
import { extractBody, formatDate } from "@/lib/format"
import { queryKeys } from "@/lib/query-keys"
import { m } from "@/paraglide/messages"
import { getLocale } from "@/paraglide/runtime"
import type { AgentFile } from "@/shared/types"

interface AgentInspectorProps {
item: AgentFile
onClose?: () => void
onAction?: (actionId: EntityActionId) => void
itemKey: string
}

export function AgentInspector({
item,
onClose,
onAction,
}: AgentInspectorProps) {
export function AgentInspector({ itemKey }: AgentInspectorProps) {
const { query: agentsQuery } = useAgentFiles("agent")
const agents = agentsQuery.data ?? []
const { activeProjectPath } = useProjectContext()
const queryClient = useQueryClient()

const { data: plugins = [] } = usePluginsQuery(activeProjectPath)
const item =
agents.find((a) => a.path === itemKey) ??
plugins
.flatMap((p) => p.contents?.agents ?? [])
.find((a) => a.path === itemKey)

const { data: itemDetail, isLoading: detailLoading } =
useAgentFileDetailQuery(item)
useAgentFileDetailQuery(item ?? null)

if (!item) return <InspectorSkeleton />

const body = itemDetail?.content ? extractBody(itemDetail.content) : ""

const handleAction = async (actionId: EntityActionId) => {
switch (actionId) {
case "open-vscode":
case "open-cursor": {
const editor = actionId === "open-vscode" ? "code" : "cursor"
const { openInEditorFn } = await import("@/server/editor")
await openInEditorFn({ data: { filePath: item.path, editor } })
break
}
case "delete": {
const { deleteItemFn } = await import("@/server/items")
await toast.promise(
deleteItemFn({
data: {
type: "agent",
name: item.name,
scope: item.scope,
projectPath: activeProjectPath,
},
}).then(() => {
queryClient.invalidateQueries({
queryKey: queryKeys.agentFiles.all,
})
queryClient.invalidateQueries({
queryKey: queryKeys.overview.all,
})
}),
{
loading: m.toast_deleting({ name: item.name }),
success: m.toast_deleted({ name: item.name }),
error: (err: unknown) =>
err instanceof Error ? err.message : m.toast_delete_failed(),
},
)
break
}
}
}

return (
<Inspector>
<InspectorHeader onClose={onClose}>
<InspectorHeader>
<InspectorTitle icon={ENTITY_ICONS.agent} title={item.name} />
<InspectorActions>
{onAction && (
<EntityActionDropdown
actions={ENTITY_ACTIONS.agent}
onAction={onAction}
itemName={item.name}
/>
)}
<EntityActionDropdown
actions={ENTITY_ACTIONS.agent}
onAction={handleAction}
itemName={item.name}
/>
</InspectorActions>
</InspectorHeader>
<InspectorBody>
Expand Down
13 changes: 6 additions & 7 deletions src/components/board/AgentsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@ import { useAgentFiles } from "@/hooks/use-config"
import type { EntityActionId } from "@/lib/entity-actions"
import { ENTITY_ACTIONS } from "@/lib/entity-actions"
import { m } from "@/paraglide/messages"
import type { DashboardDetailTarget } from "./types"

interface AgentsPanelProps {
scopeFilter?: string
onSelectItem?: (target: DashboardDetailTarget) => void
onSelectItem?: (selected: { type: string; key: string }) => void
onAction?: (
id: EntityActionId,
target: NonNullable<DashboardDetailTarget>,
selected: { type: string; key: string },
) => void
}

Expand Down Expand Up @@ -45,12 +44,12 @@ export function AgentsPanel({
return (
<div>
{filtered.map((file) => {
const target = { type: "agent" as const, agent: file }
const selected = { type: "agent", key: file.path }
return (
<EntityActionContextMenu
key={`${file.scope}-${file.name}`}
actions={ENTITY_ACTIONS.agent}
onAction={(id) => onAction?.(id, target)}
onAction={(id) => onAction?.(id, selected)}
itemName={file.frontmatter?.name ?? file.name}
>
<ListItem
Expand All @@ -64,11 +63,11 @@ export function AgentsPanel({
trailing={
<EntityActionDropdown
actions={ENTITY_ACTIONS.agent}
onAction={(id) => onAction?.(id, target)}
onAction={(id) => onAction?.(id, selected)}
itemName={file.frontmatter?.name ?? file.name}
/>
}
onClick={() => onSelectItem?.(target)}
onClick={() => onSelectItem?.(selected)}
/>
</EntityActionContextMenu>
)
Expand Down
27 changes: 14 additions & 13 deletions src/components/board/BoardLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
PlusIcon,
} from "lucide-react"
import type { ElementType } from "react"
import { useMemo, useState } from "react"
import { useMemo, useRef, useState } from "react"
import { toast } from "sonner"
import { ENTITY_ICONS } from "@/components/icons/entity-icons"
import { useProjectContext } from "@/components/ProjectContext"
Expand Down Expand Up @@ -71,9 +71,7 @@ import { EntityInspector } from "./entity-inspector"
import { FilesPanel } from "./FilesPanel"
import { LspServersPanel } from "./LspServersPanel"
import { PluginsPanel } from "./PluginsPanel"
import type { DashboardDetailTarget } from "./types"
import { useBoardConfig } from "./use-board-config"
import { useEntityActionHandler } from "./use-entity-action-handler"

const COL_CLASS = "w-[280px] min-w-[280px] shrink-0"

Expand Down Expand Up @@ -224,8 +222,13 @@ interface ColumnDef {

export function BoardLayout() {
const { activeProjectPath } = useProjectContext()
const [selected, setSelected] = useState<DashboardDetailTarget>(null)
const handleAction = useEntityActionHandler(() => setSelected(null))
const [selected, setSelected] = useState<{
type: string
key: string
} | null>(null)
// Keep last selection for Sheet close animation (content stays visible while fading out)
const lastSelected = useRef<{ type: string; key: string } | null>(null)
if (selected) lastSelected.current = selected
const [collapsedScopes, setCollapsedScopes] = useState<Set<string>>(new Set())
const { boardConfig, toggleColumn, setColumnOrder } = useBoardConfig()

Expand Down Expand Up @@ -376,7 +379,6 @@ export function BoardLayout() {
const common = {
scopeFilter: scope,
onSelectItem: setSelected as (target: unknown) => void,
onAction: handleAction as (id: string, target: unknown) => void,
}
switch (colId) {
case "files":
Expand All @@ -386,7 +388,6 @@ export function BoardLayout() {
<PluginsPanel
scopeFilter={scope}
onSelectItem={setSelected}
onAction={handleAction}
collapseSignal={pluginsCollapseSignal}
/>
)
Expand All @@ -397,7 +398,6 @@ export function BoardLayout() {
items={directMcpServers}
scopeFilter={scope}
onSelectItem={common.onSelectItem}
onAction={common.onAction}
emptyDescription={m.board_no_mcp()}
isLoading={mcpLoading}
renderTrailing={(server) => (
Expand Down Expand Up @@ -637,11 +637,12 @@ export function BoardLayout() {
>
<SheetTitle className="sr-only">Detail panel</SheetTitle>
<SheetDescription className="sr-only">Detail panel</SheetDescription>
<EntityInspector
target={selected}
onClose={() => setSelected(null)}
onAction={handleAction}
/>
{lastSelected.current && (
<EntityInspector
type={lastSelected.current.type}
itemKey={lastSelected.current.key}
/>
)}
</SheetContent>
</Sheet>
</div>
Expand Down
Loading
Loading