feat: 添加工具类命令(teleport、recap、break-cache、env、tui 等)

- /teleport: 从 claude.ai 恢复会话
- /recap: 生成会话摘要
- /break-cache: 提示缓存管理(once/always/off/status)
- /env: 环境信息展示(含密钥脱敏)
- /tui: 无闪烁 TUI 模式管理
- /onboarding: 引导流程
- /perf-issue: 性能问题诊断
- /debug-tool-call: 工具调用调试
- /usage: 用量统计(合并 /cost 和 /stats 别名)

Co-Authored-By: glm-5-turbo <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-05-09 23:04:31 +08:00
parent 6766f08e47
commit fdddb6dbe8
38 changed files with 5494 additions and 43 deletions

View File

@@ -0,0 +1,246 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { getClaudeConfigHomeDir } from '../../../utils/envUtils.js'
mock.module('bun:bundle', () => ({
feature: (_name: string) => true,
}))
mock.module('src/services/analytics/index.js', () => ({
logEvent: () => {},
stripProtoFields: (v: unknown) => v,
}))
let tmpDir: string
let claudeDir: string
const origEnv: Record<string, string | undefined> = {}
beforeEach(() => {
tmpDir = mkdtempSync(join(tmpdir(), 'tui-test-'))
claudeDir = join(tmpDir, '.claude')
mkdirSync(claudeDir, { recursive: true })
process.env.CLAUDE_CONFIG_DIR = claudeDir
// getClaudeConfigHomeDir is `memoize(...)` — clear its cache so this
// suite's CLAUDE_CONFIG_DIR overrides any value cached by an earlier
// test file in the same process.
getClaudeConfigHomeDir.cache?.clear?.()
// Save env vars we may mutate
origEnv.CLAUDE_CODE_NO_FLICKER = process.env.CLAUDE_CODE_NO_FLICKER
delete process.env.CLAUDE_CODE_NO_FLICKER
})
afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true })
delete process.env.CLAUDE_CONFIG_DIR
// Restore env vars
if (origEnv.CLAUDE_CODE_NO_FLICKER === undefined) {
delete process.env.CLAUDE_CODE_NO_FLICKER
} else {
process.env.CLAUDE_CODE_NO_FLICKER = origEnv.CLAUDE_CODE_NO_FLICKER
}
})
// Helper: invoke the command's call function
async function invokeCmd(
args: string,
): Promise<{ type: string; value: string }> {
const { callTui } = await import('../index.js')
return callTui(args) as Promise<{ type: string; value: string }>
}
describe('tui command metadata', () => {
test('has correct name, type, and description', async () => {
const mod = await import('../index.js')
const cmd = mod.default
expect(cmd.name).toBe('tui')
expect(cmd.type).toBe('local-jsx')
expect(cmd.description).toContain('flicker')
})
test('interactive and noninteractive entries are mutually gated', async () => {
const mod = await import('../index.js')
const interactiveEnabled = mod.default.isEnabled?.()
const nonInteractiveEnabled = mod.tuiNonInteractive.isEnabled?.()
expect(typeof interactiveEnabled).toBe('boolean')
expect(nonInteractiveEnabled).toBe(!interactiveEnabled)
})
test('supportsNonInteractive is true', async () => {
const mod = await import('../index.js')
const cmd = mod.tuiNonInteractive as unknown as {
supportsNonInteractive: boolean
type: string
}
expect(cmd.type).toBe('local')
expect(cmd.supportsNonInteractive).toBe(true)
})
test('local-jsx no args renders action panel without completing', async () => {
const { call } = await import('../panel.js')
const messages: string[] = []
const node = await call(
msg => {
if (msg) messages.push(msg)
},
{} as never,
'',
)
expect(node).not.toBeNull()
expect(messages).toHaveLength(0)
})
test('local-jsx explicit args completes through onDone', async () => {
const { call } = await import('../panel.js')
const messages: string[] = []
const node = await call(
msg => {
if (msg) messages.push(msg)
},
{} as never,
'status',
)
expect(node).toBeNull()
expect(messages.join('\n')).toContain('TUI Mode Status')
})
})
describe('tui status subcommand', () => {
test('reports disabled when no marker file', async () => {
const result = await invokeCmd('status')
expect(result.type).toBe('text')
expect(result.value).toContain('disabled')
})
test('reports enabled when marker file exists', async () => {
const { getTuiMarkerPath } = await import('../index.js')
const markerPath = getTuiMarkerPath()
// Write the marker
const { writeFileSync } = await import('node:fs')
writeFileSync(markerPath, '1', 'utf8')
const result = await invokeCmd('status')
expect(result.type).toBe('text')
expect(result.value).toContain('enabled')
})
})
describe('tui on subcommand', () => {
test('writes marker file', async () => {
const { getTuiMarkerPath } = await import('../index.js')
const markerPath = getTuiMarkerPath()
expect(existsSync(markerPath)).toBe(false)
const result = await invokeCmd('on')
expect(result.type).toBe('text')
expect(result.value).toContain('enabled')
expect(existsSync(markerPath)).toBe(true)
})
test('idempotent: on when already on reports already enabled', async () => {
await invokeCmd('on')
const result = await invokeCmd('on')
expect(result.type).toBe('text')
// Second call still returns a success message
expect(result.value).toContain('enabled')
})
})
describe('tui off subcommand', () => {
test('removes marker file', async () => {
const { getTuiMarkerPath } = await import('../index.js')
await invokeCmd('on')
expect(existsSync(getTuiMarkerPath())).toBe(true)
const result = await invokeCmd('off')
expect(result.type).toBe('text')
expect(result.value).toContain('disabled')
expect(existsSync(getTuiMarkerPath())).toBe(false)
})
test('off when already off returns graceful message', async () => {
const result = await invokeCmd('off')
expect(result.type).toBe('text')
expect(result.value).toContain('not active')
})
})
describe('tui toggle subcommand', () => {
test('toggle with no marker enables tui', async () => {
const { getTuiMarkerPath } = await import('../index.js')
const result = await invokeCmd('')
expect(result.type).toBe('text')
expect(result.value).toContain('enabled')
expect(existsSync(getTuiMarkerPath())).toBe(true)
})
test('toggle with marker disables tui', async () => {
const { getTuiMarkerPath } = await import('../index.js')
await invokeCmd('')
expect(existsSync(getTuiMarkerPath())).toBe(true)
const result = await invokeCmd('')
expect(result.type).toBe('text')
expect(result.value).toContain('disabled')
expect(existsSync(getTuiMarkerPath())).toBe(false)
})
})
describe('tui unknown subcommand', () => {
test('returns usage text for unknown subcommand', async () => {
const result = await invokeCmd('foobar')
expect(result.type).toBe('text')
expect(result.value).toContain('Usage')
})
})
describe('getTuiMarkerPath', () => {
test('returns path under CLAUDE_CONFIG_DIR', async () => {
const { getTuiMarkerPath } = await import('../index.js')
const p = getTuiMarkerPath()
expect(p).toContain(claudeDir)
expect(p).toContain('.tui-mode')
})
})
describe('tui status env var display', () => {
test('shows forced-on when CLAUDE_CODE_NO_FLICKER=1', async () => {
process.env.CLAUDE_CODE_NO_FLICKER = '1'
const result = await invokeCmd('status')
expect(result.value).toContain('forced on via env var')
delete process.env.CLAUDE_CODE_NO_FLICKER
})
test('shows forced-off when CLAUDE_CODE_NO_FLICKER=0', async () => {
process.env.CLAUDE_CODE_NO_FLICKER = '0'
const result = await invokeCmd('status')
expect(result.value).toContain('forced off via env var')
delete process.env.CLAUDE_CODE_NO_FLICKER
})
})
describe('isTuiModeEnabled', () => {
test('returns false when marker absent', async () => {
const { isTuiModeEnabled } = await import('../index.js')
expect(isTuiModeEnabled()).toBe(false)
})
test('returns true when marker present', async () => {
const { isTuiModeEnabled, getTuiMarkerPath } = await import('../index.js')
const { writeFileSync } = await import('node:fs')
writeFileSync(getTuiMarkerPath(), '1', 'utf8')
expect(isTuiModeEnabled()).toBe(true)
})
})

184
src/commands/tui/index.ts Normal file
View File

@@ -0,0 +1,184 @@
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { getIsNonInteractiveSession } from '../../bootstrap/state.js'
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
import type { Command, LocalCommandResult } from '../../types/command.js'
/**
* Path to the TUI-mode marker file.
*
* When this file exists, the user has opted in to flicker-free TUI mode
* (alternate screen buffer via CLAUDE_CODE_NO_FLICKER=1). The marker is
* session-independent: it persists across restarts so the user only needs to
* run `/tui on` once.
*
* Shell-profile integration: add the following to ~/.bashrc / ~/.zshrc to
* auto-enable TUI mode when the marker is present:
*
* [ -f "$HOME/.claude/.tui-mode" ] && export CLAUDE_CODE_NO_FLICKER=1
*
* Note: setting CLAUDE_CODE_NO_FLICKER at runtime cannot retroactively enter
* the alternate screen buffer — the Ink render tree is already mounted. The
* change takes effect on the NEXT session start.
*/
export function getTuiMarkerPath(): string {
return join(getClaudeConfigHomeDir(), '.tui-mode')
}
/**
* Returns true when the TUI-mode marker file is present, meaning the user has
* opted in to flicker-free alternate-screen rendering.
*/
export function isTuiModeEnabled(): boolean {
return existsSync(getTuiMarkerPath())
}
const USAGE_TEXT = [
'Usage: /tui [subcommand]',
'',
' (no args) Toggle flicker-free TUI mode (alternate screen buffer)',
' on Enable TUI mode',
' off Disable TUI mode',
' status Show current TUI mode state',
'',
'TUI mode uses the ANSI alternate screen buffer (\\x1b[?1049h) so the',
'Claude Code UI occupies a clean full-screen area with no scroll-back',
'flicker. The setting is stored in ~/.claude/.tui-mode and takes effect',
'on the next session start.',
'',
'Shell-profile integration (auto-enable on every start):',
' [ -f "$HOME/.claude/.tui-mode" ] && export CLAUDE_CODE_NO_FLICKER=1',
'',
'Environment override:',
' CLAUDE_CODE_NO_FLICKER=1 force on (overrides marker)',
' CLAUDE_CODE_NO_FLICKER=0 force off (overrides marker)',
].join('\n')
function enableTui(): LocalCommandResult {
const markerPath = getTuiMarkerPath()
mkdirSync(getClaudeConfigHomeDir(), { recursive: true })
writeFileSync(markerPath, new Date().toISOString(), 'utf8')
return {
type: 'text',
value: [
'## TUI mode enabled',
'',
`Marker written: \`${markerPath}\``,
'',
'Flicker-free alternate-screen rendering will be active on the next',
'session start. Add this to your shell profile to make it permanent:',
'',
' [ -f "$HOME/.claude/.tui-mode" ] && export CLAUDE_CODE_NO_FLICKER=1',
'',
'To disable: `/tui off`',
].join('\n'),
}
}
function disableTui(): LocalCommandResult {
const markerPath = getTuiMarkerPath()
if (!existsSync(markerPath)) {
return {
type: 'text',
value: 'TUI mode was not active.',
}
}
unlinkSync(markerPath)
return {
type: 'text',
value: [
'## TUI mode disabled',
'',
`Marker removed: \`${markerPath}\``,
'',
'Standard (non-alternate-screen) rendering will be used on the next',
'session start.',
'',
'To re-enable: `/tui on`',
].join('\n'),
}
}
export async function callTui(args: string): Promise<LocalCommandResult> {
const sub = args.trim().toLowerCase()
// ── status ──────────────────────────────────────────────────────────
if (sub === 'status') {
const enabled = isTuiModeEnabled()
const markerPath = getTuiMarkerPath()
const envVal = process.env.CLAUDE_CODE_NO_FLICKER
let envLine: string
if (envVal === '1' || envVal === 'true') {
envLine = 'CLAUDE_CODE_NO_FLICKER=1 (forced on via env var)'
} else if (envVal === '0' || envVal === 'false') {
envLine = 'CLAUDE_CODE_NO_FLICKER=0 (forced off via env var)'
} else {
envLine = 'CLAUDE_CODE_NO_FLICKER not set'
}
return {
type: 'text',
value: [
'## TUI Mode Status',
'',
` Marker file: ${enabled ? 'present' : 'absent'} (\`${markerPath}\`)`,
` Mode: ${enabled ? 'enabled' : 'disabled'}`,
` Env var: ${envLine}`,
'',
'Note: changes take effect on the next session start.',
].join('\n'),
}
}
// ── on ───────────────────────────────────────────────────────────────
if (sub === 'on') {
return enableTui()
}
// ── off ──────────────────────────────────────────────────────────────
if (sub === 'off') {
return disableTui()
}
// ── toggle (legacy default) ──────────────────────────────────────────
if (sub === '' || sub === 'toggle') {
return isTuiModeEnabled() ? disableTui() : enableTui()
}
// ── unknown subcommand ───────────────────────────────────────────────
return {
type: 'text',
value: [`Unknown subcommand: "${sub}"`, '', USAGE_TEXT].join('\n'),
}
}
const tuiCommand: Command = {
type: 'local-jsx',
name: 'tui',
description:
'Manage flicker-free TUI mode. Open actions or run: status, on, off, toggle',
isHidden: false,
isEnabled: () => !getIsNonInteractiveSession(),
argumentHint: '[status|on|off|toggle]',
bridgeSafe: true,
getBridgeInvocationError: args =>
args.trim()
? undefined
: 'Use /tui status/on/off/toggle over Remote Control.',
load: () => import('./panel.js'),
}
export const tuiNonInteractive: Command = {
type: 'local',
name: 'tui',
description:
'Toggle flicker-free TUI mode (alternate screen buffer). Subcommands: on, off, status',
isHidden: false,
isEnabled: () => getIsNonInteractiveSession(),
supportsNonInteractive: true,
bridgeSafe: true,
load: async () => ({
call: callTui,
}),
}
export default tuiCommand

100
src/commands/tui/panel.tsx Normal file
View File

@@ -0,0 +1,100 @@
import React, { useMemo, useState } from 'react';
import { Box, Dialog, Text, useInput } from '@anthropic/ink';
import type { LocalJSXCommandOnDone } from '../../types/command.js';
import { callTui } from './index.js';
type TuiAction = {
label: string;
description: string;
run: () => void;
};
const ACTION_LABEL_COLUMN_WIDTH = 24;
async function runTuiAction(subcommand: string, onDone: LocalJSXCommandOnDone): Promise<void> {
const result = await callTui(subcommand);
if (result.type === 'text') {
onDone(result.value, { display: 'system' });
}
}
function TuiPanel({ onDone }: { onDone: LocalJSXCommandOnDone }): React.ReactNode {
const [selectedIndex, setSelectedIndex] = useState(0);
const actions = useMemo<TuiAction[]>(
() => [
{
label: 'Status',
description: 'Show marker and environment override state',
run: () => void runTuiAction('status', onDone),
},
{
label: 'Toggle',
description: 'Flip persisted TUI mode for the next session',
run: () => void runTuiAction('toggle', onDone),
},
{
label: 'On',
description: 'Enable flicker-free alternate-screen mode',
run: () => void runTuiAction('on', onDone),
},
{
label: 'Off',
description: 'Disable flicker-free alternate-screen mode',
run: () => void runTuiAction('off', onDone),
},
],
[onDone],
);
const selectCurrent = () => {
const action = actions[selectedIndex];
if (!action) return;
action.run();
};
useInput((_input, key) => {
if (key.upArrow) {
setSelectedIndex(index => Math.max(0, index - 1));
return;
}
if (key.downArrow) {
setSelectedIndex(index => Math.min(actions.length - 1, index + 1));
return;
}
if (key.return) {
selectCurrent();
}
});
return (
<Dialog
title="TUI Mode"
subtitle={`${actions.length} actions`}
onCancel={() => onDone('TUI mode panel dismissed', { display: 'system' })}
color="background"
hideInputGuide
>
<Box flexDirection="column">
{actions.map((action, index) => (
<Box key={action.label} flexDirection="row">
<Text>{`${index === selectedIndex ? '' : ' '} ${action.label}`.padEnd(ACTION_LABEL_COLUMN_WIDTH)}</Text>
<Text dimColor>{action.description}</Text>
</Box>
))}
<Box marginTop={1}>
<Text dimColor>/ select · Enter run · Esc close</Text>
</Box>
</Box>
</Dialog>
);
}
export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, args?: string): Promise<React.ReactNode> {
const trimmed = args?.trim() ?? '';
if (trimmed) {
await runTuiAction(trimmed, onDone);
return null;
}
return <TuiPanel onDone={onDone} />;
}