SLOPSHOPPER

mod-explorer

Adds an /explorer command that opens a file explorer pane rooted at the folder Claude Code was started in

newpanecommandprocess
★ 8v0.2.0no licenseupdated 2026-10-10pjt3591oo/claude-code-advanced-guide/mods/mod-explorer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · mod-explorer
│ ┃ Explorer ✕ › fix the failing auth test and add an audit log call │ ┃ n: new file d: new folder r: refresh │ ┃ v app ⏺ Read(src/auth.ts) │ ┃ > src ⎿ Read 6 lines │ ┃ package.json ⏺ Update(src/auth.ts) │ ┃ README.md ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ › /explorer │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · Explorer
n: new file d: new folder r: refresh v app > src package.json README.md
README

Claude Code 완전 정복: 커맨드, 훅, 에이전트, 스킬, 플러그인 가이드

claude code란 Anthropic에서 만든 개발자용 AI 어시스턴트 입니다.

클로드 코드는 agent, skill, hook, command 등을 통해 보다 확장성 있는 LLM 워크플로우를 구축할 수 있도록 도와줍니다.

시작하며

이 글을 통해 클로드 코드를 활용하여 코드 생산 뿐 아니라, 나만의 개발 워크플로우를 자동화하고 싶은 개발자 또는 LLM을 터미널 환경에 깊숙이 통합하고 싶은 DevOps 엔지니어가 읽기 적합합니다.

주로 다루는 내용은 다음과 같습니다.

  1. slash commands
  1. hooks
  1. sub agent
  1. skill
  1. plugin

해당 내용을 실행하기 위해선 claude code 2.x 이상의 버전이 필요합니다.

만약, 1.x라면 2.x로 업데이트 해야합니다.

참고로 본문 내용에서는 2가지 형태의 코드 블록이 있습니다.

$ shell 명령어

> 클로드 코드 프롬프트

slash commands

클로드 코드는 이미 정의된 명령어 또는 사용자 정의 슬래시 명령어를 다음과 같이 사용이 가능 합니다.

사용자 정의 슬래시는 ~/.claude/commands 또는 .claude/commands 경로에 마크다운 파일로 관리됩니다.

> /[command-name] [arguments]

또는 네임스페이스를 다음과 같이 지정할 수 있습니다.

~/.claude/[NAMESPACE]/commands 또는 .claude/[NAMESPACE]/commands

> /[NAMESPACE]:[command-name] [arguments]

사용자 정의 명령어 생성

$ echo "Analyze this code for performance issues and suggest optimizations:" > .claude/commands/optimize.md
$  echo "Review this code for security vulnerabilities:" > .claude/commands/security-review.md 
$ echo 'Fix issue #$ARGUMENTS following our coding standards' > .claude/commands/fix-issue.md
$ echo 'Review PR #$1 with priority $2 and assign to $3' > .claude/commands/review-pr.md

> /fix-issue 123 high-priority 

/fix-issue is running… 123 high-priority 

⏺ I'll fix issue #123 with high priority following your coding standards.

  Let me start by fetching the issue details from GitHub.

⏺ Bash(gh issue view 123 --json title,body,labels,state,comments)
  ⎿  Running…

──────────────────────────────────────────────────────────────────────────────────────
 Bash command

   gh issue view 123 --json title,body,labels,state,comments
   Fetch issue #123 details from GitHub

 Do you want to proceed?
 ❯ 1. Yes
   2. Yes, and don't ask again for gh issue view commands in
   /Users/jeongtae/Desktop/claude-code-advanced-guide
   3. No, and tell Claude what to do differently (esc)
> /review-pr 456 high alice 

/review-pr is running… 456 high alice 

⏺ I'll review PR #456 with high priority and prepare it for assignment to alice.

  Let me start by fetching the PR information using the GitHub CLI.

⏺ Bash(gh pr view 456 --json
      title,body,author,state,headRefName,baseRefName,files,commits)
  ⎿  Interrupted · What should Claude do instead?

파일 참조

@를 이용하면 파일을 참조할 수 있습니다.

$ echo 'Compare @src/need_debugger.py with @src/need_review.py' > .claude/commands/referrence-file.md 
> /referrence-file

/referrence-file is running…

⏺ I'll compare these two Python files for you. Let me read both files first.

⏺ Read(src/need_debugger.py)
  ⎿  Error reading file

⏺ Read(src/need_review.py)
  ⎿  Error reading file

⏺ The files don't exist at those paths. Let me search for them in the repository.

⏺ Search(pattern: "**/need_debugger.py")
  ⎿  Found 1 file (ctrl+o to expand)

⏺ Search(pattern: "**/need_review.py")
  ⎿  Found 1 file (ctrl+o to expand)

⏺ Found them! They're in the root directory. Let me read both files now.

⏺ Read(need_debugger.py)
  ⎿  Read 12 lines

⏺ Read(need_review.py)
  ⎿  Read 7 lines

⏺ Comparison of need_debugger.py vs need_review.py

  need_debugger.py (need_debugger.py:1-12)

  def main() -> None:
      student = {
          "name": "Alice",
          "age": 21,
          "courses": ["Math", "Science", "Art"]
      }

      print(student["a"])  # BUG: KeyError


  if __name__ == "__main__":
      main()

  Issues:
  - Bug at line 8: Attempts to access student["a"] which doesn't exist, causing a
  KeyError
  - Should likely be student["age"] or student["name"]

  need_review.py (need_review.py:1-7)

  def main() -> None:
      print("Hello, World!")


  if __name__ == "__main__":
      main()

  Characteristics:
  - Simple "Hello, World!" program
  - No bugs, but basic implementation
  - Could potentially benefit from code review for production standards

  Key Differences:

  1. Complexity: need_debugger.py has more complex logic with a dictionary
  structure, while need_review.py is minimal
  2. Purpose: need_debugger.py clearly has a bug that needs debugging, while
  need_review.py is functional but basic
  3. Error state: need_debugger.py will crash at runtime, need_review.py runs
  successfully

  The naming suggests these files are examples for testing specialized agents -
  one needs the debugger agent (has a bug), while the other needs the
  code-reviewer agent (works but could be reviewed for quality).

Bash 명령어 실행

커맨드 정의를 위한 마크다운 파일에 !배시 명령어 형태로 배시 명령어를 추가할 수 있습니다.

  • 구조
---
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*)
argument-hint: [message]
description: Create a git commit
model: claude-3-5-haiku-20241022
---

Create a git commit with message: $ARGUMENTS

allowed-tools: 명령어가 사용할 수 있는 도구 목록

argument-hint: 슬래시 명령어에 필요한 인수

description: 명령어의 간단한 설명

model: 특정 모델 문자열

disable-model-invocation: 슬래시 명령어가 호출하지 말아야 하는 명령어

  • 깃 변경 사항을 읽어 커밋 메시지 만들기

.claude/commands/bash-command.md

---
allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*)
description: Create a git commit
---

## Context

- Current git status: !`git status`
- Current git diff (staged and unstaged changes): !`git diff HEAD`
- Current branch: !`git branch --show-current`
- Recent commits: !`git log --oneline -10`

## Your task

Based on the above changes, create a single git commit.
  • 명령어 사용
> /bash-command

> /bash-command is running… 
  ⎿  Allowed 3 tools for this command

⏺ I can see there's a modification to README.md that adds documentation about bash
   command execution in command definitions. Let me create a commit for this
  change.

⏺ Bash(git add README.md && git commit -m "Add documentation for bash command
      execution in custom commands…)
  ⎿ [main 5fb2eb9] Add documentation for bash command execution in custom command
    s
     1 file changed, 22 insertions(+)


⏺ Bash(git status)
  ⎿  On branch main                                              
     Changes not staged for commit:
       (use "git add <file>..." to update what will be committed)
     … +4 lines (ctrl+o to expand)

⏺ Commit created successfully! The changes to README.md have been committed with
  the message documenting the new bash command execution feature.

  I notice there's also a modification to .claude/bash-command-log.txt, which
  appears to be a log file. Would you like me to commit that as well, or should we
   ignore log files?

깃 로그를 살펴보면 다음과 같이 로그가 생성된 것을 확인할 수 있습니다.

$ git log

commit 5fb2eb93632702846b8d7f1bf344cba7f97fa2fc (HEAD -> main)
Author: pjt3591oo <pjt3591oo@maskit.co.kr>
Date:   Thu Oct 30 04:48:28 2025 +0900

    Add documentation for bash command execution in custom commands
    
    Added Korean documentation explaining how to use !`bash command` syntax in command definition markdown files, with a practical example showing git-related commands.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    Co-Authored-By: Claude <noreply@anthropic.com>

commit 72c2cda2ec614f5f3ef28e56262e9c6e44a1046a
Author: pjt3591oo <pjt3591oo@maskit.co.kr>
Date:   Thu Oct 30 04:47:24 2025 +0900

    first commit

네이티브 명령어 지원

클로드 코드는 네이티브 수준에서 기본 명령어를 제공합니다.

/mcp, /plugin, /hook, /agents 등

사용 가능한 명령어는 클로드 프롬프트에 /를 입력하면 사용 가능한 명령어 목록을 확인할 수 있습니다.

──────────────────────────────────────────────────────────────────────────────────────
> / 
──────────────────────────────────────────────────────────────────────────────────────
  /doctor                          Diagnose and verify your Claude Code installation
                                   and settings
  /exit (quit)                     Exit the REPL
  /export                          Export the current conversation to a file or
                                   clipboard
  /feedback (bug)                  Submit feedback about Claude Code
  /help                            Show help and available commands
  /hooks                           Manage hook configurations for tool events
  /ide                             Manage IDE integrations and show status
  /init                            Initialize a new CLAUDE.md file with codebase
                                   documentation
  /install-github-app              Set up Claude GitHub Actions for a repository
  /login                           Sign in with your Anthropic account

hooks

/hooks 명령어를 통해 훅 관리 가능

이벤트

PreToolUse: 도구 호출 전에 실행됩니다 (차단 가능)
PostToolUse: 도구 호출 완료 후 실행됩니다
UserPromptSubmit: 사용자가 프롬프트를 제출할 때, Claude가 처리하기 전에 실행됩니다
Notification: Claude Code가 알림을 보낼 때 실행됩니다
Stop: Claude Code가 응답을 완료할 때 실행됩니다
SubagentStop: 하위 에이전트 작업이 완료될 때 실행됩니다
PreCompact: Claude Code가 압축 작업을 실행하려고 할 때 실행됩니다
SessionStart: Claude Code가 새 세션을 시작하거나 기존 세션을 재개할 때 실행됩니다
SessionEnd: Claude Code 세션이 종료될 때 실행됩니다

훅 추가

> /hook

╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Hook Configuration                                                                                               │
│                                                                                                                  │
│ Hooks are shell commands you can register to run during Claude Code processing. Docs                             │
│ • Each hook event has its own input and output behavior                                                          │
│ • Multiple hooks can be registered per event, executed in parallel                                               │
│ • Any changes to hooks outside of /hooks require a restart                                                       │
│ • Timeout: 60 seconds                                                                                            │
│                                                                                                                  │
│ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ │ ⚠ CRITICAL SECURITY WARNING - USE AT YOUR OWN RISK                                                             │
│ │ Hooks execute arbitrary shell commands with YOUR full user permissions without confirmation.                   │
│ │ • You are SOLELY RESPONSIBLE for ensuring your hooks are safe and secure                                       │
│ │ • Hooks can modify, delete, or access ANY files your user account can access                                   │
│ │ • Malicious or poorly written hooks can cause irreversible data loss or system damage                          │
│ │ • Anthropic provides NO WARRANTY and assumes NO LIABILITY for any damages resulting from hook usage            │
│ │ • Only use hooks from trusted sources to prevent data exfiltration                                             │
│ │ • Review the hooks documentation before proceeding                                                             │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│                                                                                                                  │
│ Select hook event:                                                                                               │
│ ❯ 1.  PreToolUse - Before tool execution                                                                         │
│   2.  PostToolUse - After tool execution                                                                         │
│   3.  Notification - When notifications are sent                                                                 │
│   4.  UserPromptSubmit - When the user submits a prompt                                                          │
│ ↓ 5.  SessionStart - When a new session is started                                                               │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
   Enter to acknowledge risks and continue · Esc to exit

select hook event: 목록중 하나 적절히 선택 1번을 선택하면 다음과 같이 1. + add new matcher 를 선택 하라고 나옴.

╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ PreToolUse - Tool Matchers                                                                                       │
│                                                                                                                  │
│ Input to command is JSON of tool call arguments.                                                                 │
│ Exit code 0 - stdout/stderr not shown                                                                            │
│ Exit code 2 - show stderr to model and block tool call                                                           │
│ Other exit codes - show stderr to user only but continue with tool call                                          │
│                                                                                                                  │
│ ❯ 1. + Add new matcher…  No matchers configured yet                                                              │
│                                                                                                                  │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
   Enter to select · Esc to go back

1번을 선택하면 다음과 같이 PreToolUser는 matcher로 지정된 툴(여기서의 툴은 권한을 의미합니다.)이 호출되면, 특정 동작을 수행합니다.

여기서는 Bash 를 명시합니다. 만약 모든 도구를 사용허가를 한다면 * 입력하면 됩니다.

다음과 같이 실제 어떤 동작을 할지 추가하라고 나옵니다.

╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ PreToolUse - Matcher: bash                                                                                       │
│                                                                                                                  │
│ Input to command is JSON of tool call arguments.                                                                 │
│ Exit code 0 - stdout/stderr not shown                                                                            │
│ Exit code 2 - show stderr to model and block tool call                                                           │
│ Other exit codes - show stderr to user only but continue with tool call                                          │
│                                                                                                                  │
│ ❯ 1. + Add new hook…  No hooks configured yet                                                                    │
│                                                                                                                  │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
   Enter to select · Esc to go back

1번을 선택하면 훅의 실질적인 동작을 정의할 수 있습니다.

╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Add new hook                                                                                                                                     │
│                                                                                                                                                  │
│ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ │ ⚠ CRITICAL SECURITY WARNING                                                                                                                    │
│ │ Hooks execute arbitrary shell commands with YOUR full user permissions. By proceeding, you acknowledge:                                        │
│ │ • You are SOLELY responsible for any commands you configure                                                                                    │
│ │ • Hooks can modify, delete, or access ANY files your user can access                                                                           │
│ │ • Anthropic provides NO WARRANTY and assumes NO LIABILITY for damages                                                                          │
│ │ • USE AT YOUR OWN RISK - Test thoroughly before production use                                                                                 │
│ │ • Review the hooks documentation before proceeding                                                                                             │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│                                                                                                                                                  │
│ Event: PreToolUse - Before tool execution                                                                                                        │
│                                                                                                                                                  │
│ Input to command is JSON of tool call arguments.                                                                                                 │
│ Exit code 0 - stdout/stderr not shown                                                                                                            │
│ Exit code 2 - show stderr to model and block tool call                                                                                           │
│ Other exit codes - show stderr to user only but continue with tool call                                                                          │
│                                                                                                                                                  │
│ Matcher: bash                                                                                                                                    │
│                                                                                                                                                  │
│ Command:                                                                                                                                         │
│                                                                                                                                                  │
│ ╭────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│ │                                                                                                                                                │
│ ╰────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────│
│                                                                                                                                                  │
│ Examples:                                                                                                                                        │
│ • jq -r '.tool_input.file_path | select(endswith(".go"))' | xargs -r gofmt -w                                                                    │
│ • jq -r '.tool_input | \"[\" + (.description // \"No description\") + \"] \" + .command' >> \"$CLAUDE_PROJECT_DIR/.claude/bash-command-log.txt\" │
│ • /usr/local/bin/security_check.sh                                                                                                               │
│ • python3 ~/hooks/validate_changes.py                                                                                                            │
│                                                                                                                                                  │
│                                                                                                                                                  │
│ ⚠ Security Best Practices:                                                                                                                       │
│ • Use absolute paths for custom scripts (~/scripts/check.sh not check.sh)                                                                        │
│ • Avoid using sudo - hooks run with your user permissions                                                                                        │
│ • Be cautious with patterns that match sensitive files (.env, .ssh/*, secrets.*)                                                                 │
│ • Validate and sanitize input paths (reject ../ paths, check expected formats)                                                                   │
│ • Avoid piping untrusted content to shells (curl ... | sh, | bash)                                                                               │
│ • Use restrictive file permissions (chmod 644, not 777)                                                                                          │
│ • Quote all variable expansions to prevent injection: "$VAR"                                                                                     │
│ • Keep error checking enabled in scripts (avoid set +e)                                                                                          │
│ By adding this hook, you accept all responsibility for its execution and any consequences.                                                       │
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
   Enter to confirm · Esc to cancel

여기서는 다음과 같이 정의하도록 했습니다. 해당 명령어는 실행된 명령어를 ./.claude/bash-command-log.txt에 저장합니다.

jq -r '.tool_input | \"[\" + (.description // \"No description\") + \"] \" + .command' >> \"$CLAUDE_PROJECT_DIR/.claude/bash-command-log.txt\"

훅을 정의하면 어느 경로에서 관리할지 나옵니다. 여기서는 적절히 원하는 경로를 선택하면 됩니다.

╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
│ Save hook configuration                                                                                                                          │
│                                                                                                                                                  │
│   Event: PreToolUse - Before tool execution                                                                                                      │
│   Matcher: bash                                                                                                                                  │
│   Command: jq -r '"\(.tool_input.command) - \(.tool_input.description // "No description")"' >> ~/.claude/bash-command-log.txt                   │
│                                                                                                                                                  │
│ Where should this hook be saved?                                                                                                                 │
│                                                                                                                                                  │
│ ❯ 1. Project settings (local)   Saved in .claude/settings.local.json                                                                             │
│   2. Project settings           Checked in at .claude/settings.json                                                                
Source 1 files
hooks/register.js 243 lines
1// my-mod/hooks/register.js
2// /explorer : Claude Code를 실행한 폴더를 루트로 하는 파일 탐색기 Pane
3//  - 루트 밖으로는 나갈 수 없음
4//  - 폴더는 열고 닫을 수 있음 (v 열림 / > 닫힘)
5//  - n: 새 파일, d: 새 폴더, r: 새로고침
6//  - 새 파일/폴더는 Enter를 누른 시점에 "마지막으로 선택된 경로" 기준으로 생성
7
8const PANE = 'explorer'
9const ICON_OPEN = 'v'
10const ICON_CLOSED = '>'
11const MAX_ROWS = 500
12
13// ── 상태 (모듈 변수: 코드를 저장해 핫리로드되면 초기화됨) ──
14let root = ''              // 세션 시작 시점의 실행 경로 (고정)
15let error = ''             // pane에 빨간 글씨로 보여줄 오류
16let selected = ''          // 마지막으로 누른 행의 경로 (없으면 root가 대상)
17let selectedIsDir = true
18let creating = null        // { kind: 'file' | 'dir' }  (위치는 생성 시점에 계산)
19let draft = ''             // 입력창에 쓰던 이름 (행을 선택해도 유지)
20const expanded = new Set() // 펼쳐진 폴더 경로
21const cache = new Map()    // 폴더 경로 -> [{ name, isDir, isLink }]
22
23// ── 경로 유틸 (항상 '/' 구분자, 끝 슬래시 없음) ──
24const norm = (p) => String(p).replace(/\\/g, '/').replace(/\/+$/, '')
25const joinPath = (base, name) => norm(base) + '/' + name
26const dirOf = (p) => norm(p).slice(0, norm(p).lastIndexOf('/'))
27const baseName = (p) => norm(p).split('/').pop() || norm(p)
28const isWindows = () => /^[A-Za-z]:/.test(root)
29const errText = (err) => String(err && err.message ? err.message : err)
30
31// 글자/숫자/공백/_ - . 만 허용 (cmd에 넘기는 mkdir 인자를 안전하게 유지)
32const validName = (n) =>
33  /^[\p{L}\p{N}_. \-]+$/u.test(n) && !/^\.+$/.test(n) && n === n.trim() && !n.endsWith('.')
34
35// 루트 자신이거나 루트 아래 경로일 때만 true (Windows 대소문자 무시)
36function isInside(p) {
37  const a = norm(p).toLowerCase()
38  const r = root.toLowerCase()
39  return a === r || a.startsWith(r + '/')
40}
41
42// 생성 위치: 폴더를 선택했으면 그 폴더, 파일이면 그 파일이 있는 폴더, 없으면 루트
43function targetParent() {
44  if (!selected) return root
45  return selectedIsDir ? selected : dirOf(selected)
46}
47
48// $를 최상위 함수에 넘기는 것은 mod 검증 규칙상 허용됨
49async function loadDir($, target) {
50  if (!isInside(target)) {
51    error = 'outside root: ' + target
52    return
53  }
54  try {
55    const raw = await $.fs.list(target) // [{ name, kind, size, isLink }]
56    const rows = raw
57      .filter((it) => it.name && it.name !== '.' && it.name !== '..')
58      .map((it) => ({
59        name: it.name,
60        isLink: !!it.isLink,
61        isDir: /dir|folder/i.test(String(it.kind)) && !it.isLink, // 링크는 펼치지 않음
62      }))
63    rows.sort((a, b) => (a.isDir === b.isDir ? a.name.localeCompare(b.name) : a.isDir ? -1 : 1))
64    cache.set(target, rows)
65    error = ''
66  } catch (err) {
67    error = errText(err)
68  }
69}
70
71// 실패하면 오류 문자열, 성공하면 '' 를 돌려줌
72async function createEntry($, kind, parent, rawName) {
73  const name = String(rawName).trim()
74  if (!validName(name)) return 'invalid name'
75  if (!isInside(parent)) return 'outside root'
76  const full = joinPath(parent, name)
77  try {
78    if (await $.fs.exists(full)) return 'already exists: ' + name
79    if (kind === 'file') {
80      await $.fs.write(full, '')
81    } else {
82      // $.process.run은 셸을 쓰지 않으므로 Windows에서는 cmd /c 로 mkdir을 호출
83      const native = isWindows() ? full.replace(/\//g, '\\') : full
84      const argv = isWindows() ? ['cmd', '/c', 'mkdir', native] : ['mkdir', native]
85      const r = await $.process.run(argv)
86      if (r.exitCode !== 0) return (r.stderr || 'mkdir failed').trim()
87    }
88  } catch (err) {
89    return errText(err)
90  }
91  expanded.add(parent) // 만든 항목이 보이도록 대상 폴더를 펼침
92  await loadDir($, parent)
93  return ''
94}
95
96// 루트를 맨 위 행으로 두고, 펼쳐진 폴더만 아래로 풀어서 한 줄 목록으로 만든다
97function flatten() {
98  const out = [{ name: baseName(root), isDir: true, full: root, depth: 0 }]
99  const walk = (path, depth) => {
100    for (const it of cache.get(path) ?? []) {
101      if (out.length >= MAX_ROWS) return
102      const full = joinPath(path, it.name)
103      out.push({ ...it, full, depth })
104      if (it.isDir && expanded.has(full)) walk(full, depth + 1)
105    }
106  }
107  if (expanded.has(root)) walk(root, 1)
108  return out
109}
110
111export function register(on) {
112  // 세션 시작: 루트 고정 + /explorer 명령 등록
113  on('session.start', async ($, e, next) => {
114    if (!root) root = norm(await $.session.cwd())
115    await $.command.register({ name: 'explorer', description: 'Open the file explorer pane' })
116    return next(e)
117  })
118
119  // /explorer 실행: 루트를 펼친 상태로 읽고 Pane을 연다
120  on('command.run', { command: 'explorer' }, async ($) => {
121    if (!root) root = norm(await $.session.cwd())
122    if (!cache.has(root)) {
123      expanded.add(root)
124      await loadDir($, root)
125    }
126    await $.ui.open({ id: PANE, title: 'Explorer', focus: true, closeOnEscape: true })
127    return {}
128  })
129
130  // Pane 그리기
131  on('ui.render', { component: 'Pane' }, async ($, e, next) => {
132    if (e.requestId !== PANE) return next(e)
133    const { Box, Text, Button, Input } = $.ui.resolve(e)
134    const redraw = () => $.ui.invalidate('ui.render')
135
136    const toggle = async (full) => {
137      if (expanded.has(full)) {
138        expanded.delete(full)
139      } else {
140        expanded.add(full)
141        if (!cache.has(full)) await loadDir($, full)
142      }
143      redraw()
144    }
145
146    const refresh = async () => {
147      cache.clear()
148      await loadDir($, root)
149      for (const p of [...expanded]) await loadDir($, p)
150      redraw()
151    }
152
153    const startCreate = (kind) => {
154      creating = { kind }
155      draft = ''
156      error = ''
157      redraw()
158    }
159
160    // 파일/폴더 행: 누르면 선택되고, 폴더면 토글
161    const rows = flatten().map((it, i) => {
162      const mark = it.isDir ? (expanded.has(it.full) ? ICON_OPEN : ICON_CLOSED) : ' '
163      const tail = it.full === selected ? ' <' : it.isLink ? ' @' : ''
164      return Button({
165        key: 'n-' + i,
166        plain: true,
167        label: '  '.repeat(it.depth) + mark + ' ' + it.name + tail,
168        onPress: () => {
169          selected = it.full
170          selectedIsDir = it.isDir
171          if (it.isDir) return toggle(it.full)
172          redraw()
173        },
174      })
175    })
176
177    // 새 파일/폴더 입력창. 위치는 매 렌더마다, 그리고 Enter 시점에 다시 계산
178    const parent = targetParent()
179    const form = creating
180      ? [
181          Text({
182            dimColor: true,
183            children: [
184              (creating.kind === 'file' ? 'new file in ' : 'new folder in ') +
185                (parent.slice(root.length + 1) || '.'),
186            ],
187          }),
188          Input({
189            key: 'create-name',
190            label: creating.kind === 'file' ? 'File' : 'Folder',
191            placeholder: 'name, then Enter',
192            value: draft,
193            submitLabel: 'create',
194            autoFocus: true,
195            onInput: (value) => {
196              draft = value
197            },
198            onSubmit: async (value) => {
199              // Enter를 누른 시점에 선택된 경로를 다시 읽는다
200              const target = targetParent()
201              const msg = await createEntry($, creating.kind, target, value)
202              if (msg) {
203                error = msg
204              } else {
205                creating = null
206                draft = ''
207                error = ''
208              }
209              redraw()
210            },
211          }),
212          Button({
213            key: 'create-cancel',
214            label: 'cancel',
215            plain: true,
216            onPress: () => {
217              creating = null
218              draft = ''
219              redraw()
220            },
221          }),
222        ]
223      : []
224
225    return Box({
226      flexDirection: 'column',
227      children: [
228        Box({
229          flexDirection: 'row',
230          columnGap: 3,
231          children: [
232            Button({ key: 'new-file', label: 'new file', hotkey: 'n', plain: true, onPress: () => startCreate('file') }),
233            Button({ key: 'new-dir', label: 'new folder', hotkey: 'd', plain: true, onPress: () => startCreate('dir') }),
234            Button({ key: 'refresh', label: 'refresh', hotkey: 'r', plain: true, onPress: refresh }),
235          ],
236        }),
237        ...form,
238        ...(error ? [Text({ color: 'red', wrap: 'wrap', children: [error] })] : []),
239        ...rows,
240      ],
241    })
242  })
243}