Skip to content

Commit fec7943

Browse files
Remove live-reload watcher to keep the panel PR focused
Per review on #1289: the filesystem watcher is an independently reviewable feature and moves to its own PR (recursive-watch semantics differ per platform). The panel keeps its refresh-on-open and refresh-after-delete behavior, which covers installs and edits without any watcher. Also drops ponytail-compat.test.ts (external dependency, no CI signal). 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
1 parent 9f030e0 commit fec7943

6 files changed

Lines changed: 12 additions & 171 deletions

File tree

cli/src/chat.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -557,9 +557,9 @@ export const Chat = ({
557557
const askUserState = useChatStore((state) => state.askUserState)
558558

559559
// Get loaded skills for slash commands. Keyed on the registry version so a
560-
// live-reload (watcher or /skills refresh) swaps the list without a
561-
// restart — the registry object itself is mutated in place, which zustand
562-
// and useMemo would never notice.
560+
// mid-session change (delete via the panel, edit on disk + reopen) swaps the
561+
// list without a restart — the registry object itself is mutated in place,
562+
// which zustand and useMemo would never notice.
563563
const skillsVersion = useSyncExternalStore(
564564
subscribeToSkillsVersion,
565565
getSkillsVersion,
@@ -1270,8 +1270,7 @@ export const Chat = ({
12701270
}, [closeSkillsPanel, setInputFocused, inputRef])
12711271

12721272
// Refresh the registry when the /skills panel opens, so a skill installed
1273-
// moments ago shows up even if the watcher missed it (e.g. directory
1274-
// created and populated before the watcher could arm on it).
1273+
// or edited moments ago shows up without restarting the CLI.
12751274
useEffect(() => {
12761275
if (!skillsPanelOpen) return
12771276
void refreshSkillRegistry()

cli/src/components/skills-panel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,8 @@ export const SkillsPanel: React.FC<SkillsPanelProps> = ({
137137
try {
138138
await rm(skillDir, { recursive: true })
139139
// Refresh the registry right away: the version bump re-renders the
140-
// panel with the refreshed list (the watcher would also catch it, but
141-
// a whole-skills-directory delete deserves instant feedback).
140+
// panel with the refreshed list instead of waiting for the
141+
// refresh-on-open.
142142
void refreshSkillRegistry()
143143
// Dropping the row moves the cursor to whatever fills the vacancy.
144144
const successor = filtered[selectedIndex + 1] ?? filtered[selectedIndex - 1]

cli/src/index.tsx

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,7 @@ import {
4747
} from './utils/renderer-cleanup'
4848
import { startTerminalWatchdog } from './utils/terminal-watchdog'
4949
import { installTerminalProtocolController } from './utils/terminal-protocol-controller'
50-
import {
51-
initializeSkillRegistry,
52-
startSkillDirWatcher,
53-
} from './utils/skill-registry'
50+
import { initializeSkillRegistry } from './utils/skill-registry'
5451
import { detectTerminalTheme } from './utils/terminal-color-detection'
5552
import { setOscDetectedTheme } from './utils/theme-system'
5653

@@ -266,11 +263,6 @@ async function main(): Promise<void> {
266263
// Initialize skill registry (loads skills from .agents/skills)
267264
await initializeSkillRegistry()
268265

269-
// Claude Code parity: watch skill directories so skills installed, edited,
270-
// or deleted mid-session appear without a restart. A no-op when none of the
271-
// directories exist yet.
272-
startSkillDirWatcher()
273-
274266
// Handle publish command before rendering the app
275267
if (isPublishCommand) {
276268
const publishIndex = process.argv.indexOf('publish')

cli/src/utils/__tests__/ponytail-compat.test.ts

Lines changed: 0 additions & 72 deletions
This file was deleted.

cli/src/utils/__tests__/skill-registry.test.ts

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
getSkillsVersion,
1111
initializeSkillRegistry,
1212
refreshSkillRegistry,
13-
startSkillDirWatcher,
1413
subscribeToSkillsVersion,
1514
} from '../skill-registry'
1615

@@ -37,8 +36,6 @@ const writeSkill = (
3736
return skillDir
3837
}
3938

40-
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
41-
4239
beforeEach(() => {
4340
tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-registry-'))
4441
setProjectRoot(tmpRoot)
@@ -138,28 +135,6 @@ describe('skill-registry refresh', () => {
138135
await refreshSkillRegistry()
139136
expect(onChange).toHaveBeenCalledTimes(1)
140137
})
141-
142-
test('watcher picks up an install without restart', async () => {
143-
// The skills directory must exist BEFORE the watcher arms on it — fs.watch
144-
// cannot watch a directory that does not exist. (A directory created and
145-
// populated later is caught by the refresh-on-panel-open instead.)
146-
fs.mkdirSync(path.join(tmpRoot, '.agents', 'skills'), { recursive: true })
147-
startSkillDirWatcher()
148-
149-
await initializeSkillRegistry()
150-
expect(getSkillCountForTest()).toBe(0)
151-
152-
writeSkill('project', 'deploy', {
153-
name: 'deploy',
154-
description: 'Deploy the app',
155-
})
156-
157-
// Watcher debounce is 300ms; give it room on slow CI.
158-
await wait(900)
159-
160-
expect(getSkillsVersion()).toBeGreaterThan(0)
161-
expect(getLoadedSkills()['deploy']).toBeDefined()
162-
})
163138
})
164139

165140
function getSkillCountForTest(): number {

cli/src/utils/skill-registry.ts

Lines changed: 5 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import os from 'os'
2-
import { watch, type FSWatcher } from 'fs'
32

4-
import { loadSkills as sdkLoadSkills, resolveSkillsDirs } from '@codebuff/sdk'
3+
import { loadSkills as sdkLoadSkills } from '@codebuff/sdk'
54

65
import { getProjectRoot, tryGetProjectRoot } from '../project-files'
76
import { logger } from './logger'
@@ -93,62 +92,11 @@ export async function refreshSkillRegistry(): Promise<boolean> {
9392
}
9493

9594
// ============================================================================
96-
// Live reload (Claude Code parity)
95+
// Live reload
9796
// ============================================================================
98-
// Claude Code watches skill directories and picks up add/edit/delete within
99-
// the running session. Without this, a skill installed mid-session is
100-
// invisible until restart — the "install every time" complaint.
101-
102-
const SKILLS_WATCH_DEBOUNCE_MS = 300
103-
104-
let skillWatchers: FSWatcher[] = []
105-
106-
/**
107-
* Start watching the resolved skill directories (global + project, Claude
108-
* locations included). Idempotent. A directory that does not exist yet is
109-
* skipped — installs that create it later are caught by the refresh when the
110-
* /skills panel opens.
111-
*/
112-
export function startSkillDirWatcher(): void {
113-
if (skillWatchers.length > 0) return
114-
115-
const cwd = skillsCwd()
116-
const homeDir = os.homedir()
117-
const dirs = resolveSkillsDirs({ cwd, homeDir })
118-
119-
let debounceTimer: ReturnType<typeof setTimeout> | null = null
120-
const scheduleRefresh = () => {
121-
if (debounceTimer) clearTimeout(debounceTimer)
122-
debounceTimer = setTimeout(() => {
123-
debounceTimer = null
124-
void refreshSkillRegistry()
125-
}, SKILLS_WATCH_DEBOUNCE_MS)
126-
}
127-
128-
for (const dir of dirs) {
129-
try {
130-
const watcher = watch(dir, { persistent: false }, () => {
131-
// Filter nothing: skill installs create directories AND write
132-
// SKILL.md inside them, whole-skill deletes only touch the dir name,
133-
// and the refresh itself is a debounced handful of stat+read calls.
134-
// Simpler and correct beats a filename heuristic that misses cases.
135-
scheduleRefresh()
136-
})
137-
watcher.on('error', (error) => {
138-
logger.warn({ error }, `Skill watcher error for ${dir}`)
139-
})
140-
skillWatchers.push(watcher)
141-
} catch {
142-
// Directory does not exist (e.g. no ~/.agents/skills yet). Nothing to
143-
// watch; installs create it fresh and a restart picks them up.
144-
}
145-
}
146-
}
147-
148-
export function stopSkillDirWatcher(): void {
149-
for (const watcher of skillWatchers) watcher.close()
150-
skillWatchers = []
151-
}
97+
// Implemented on feat/skills-reload — deliberately not here. Watching skill
98+
// directories is an independently reviewable feature (recursive watch
99+
// semantics differ per platform) and lives in its own PR.
152100

153101
/**
154102
* Initialize the skill registry by loading skills via the SDK.
@@ -237,7 +185,6 @@ export function getLoadedSkillsMessage(): string | null {
237185
export function __resetSkillRegistryForTests(): void {
238186
skillsCache = {}
239187
skillsVersion = 0
240-
stopSkillDirWatcher()
241188
}
242189

243190
/**

0 commit comments

Comments
 (0)