mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
feat: 添加 summary 命令 TypeScript 重写与其他命令增强
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -180,6 +180,8 @@ import mockLimits from './commands/mock-limits/index.js'
|
||||
import bridgeKick from './commands/bridge-kick.js'
|
||||
import version from './commands/version.js'
|
||||
import summary from './commands/summary/index.js'
|
||||
import skillLearning from './commands/skill-learning/index.js'
|
||||
import skillSearch from './commands/skill-search/index.js'
|
||||
import {
|
||||
resetLimits,
|
||||
resetLimitsNonInteractive,
|
||||
@@ -274,7 +276,6 @@ export const INTERNAL_ONLY_COMMANDS = [
|
||||
goodClaude,
|
||||
issue,
|
||||
initVerifiers,
|
||||
...(forceSnip ? [forceSnip] : []),
|
||||
mockLimits,
|
||||
bridgeKick,
|
||||
version,
|
||||
@@ -283,7 +284,6 @@ export const INTERNAL_ONLY_COMMANDS = [
|
||||
resetLimitsNonInteractive,
|
||||
onboarding,
|
||||
share,
|
||||
summary,
|
||||
teleport,
|
||||
antTrace,
|
||||
perfIssue,
|
||||
@@ -397,6 +397,10 @@ const COMMANDS = memoize((): Command[] => [
|
||||
...(torch ? [torch] : []),
|
||||
...(daemonCmd ? [daemonCmd] : []),
|
||||
...(jobCmd ? [jobCmd] : []),
|
||||
...(forceSnip ? [forceSnip] : []),
|
||||
summary,
|
||||
skillLearning,
|
||||
skillSearch,
|
||||
...(process.env.USER_TYPE === 'ant' && !process.env.IS_DEMO
|
||||
? INTERNAL_ONLY_COMMANDS
|
||||
: []),
|
||||
|
||||
@@ -54,7 +54,6 @@ function BridgeToggle({ onDone, name }: Props): React.ReactNode {
|
||||
const replBridgeOutboundOnly = useAppState(s => s.replBridgeOutboundOnly)
|
||||
const [showDisconnectDialog, setShowDisconnectDialog] = useState(false)
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: bridge starts once, should not restart on state changes
|
||||
useEffect(() => {
|
||||
// If already connected or enabled in full bidirectional mode, show
|
||||
// disconnect confirmation. Outbound-only (CCR mirror) doesn't count —
|
||||
|
||||
@@ -5,7 +5,7 @@ export default {
|
||||
type: 'local-jsx',
|
||||
name: 'effort',
|
||||
description: 'Set effort level for model usage',
|
||||
argumentHint: '[low|medium|high|max|auto]',
|
||||
argumentHint: '[low|medium|high|xhigh|max|auto]',
|
||||
get immediate() {
|
||||
return shouldInferenceConfigCommandBeImmediate()
|
||||
},
|
||||
|
||||
@@ -52,7 +52,7 @@ const forceSnip = {
|
||||
name: 'force-snip',
|
||||
description: 'Force snip conversation history at current point',
|
||||
supportsNonInteractive: true,
|
||||
isHidden: true,
|
||||
isHidden: false,
|
||||
load: () => Promise.resolve({ call }),
|
||||
} satisfies Command
|
||||
|
||||
|
||||
@@ -3058,7 +3058,6 @@ const usageReport: Command = {
|
||||
|
||||
// Show collection message if collecting
|
||||
if (collectRemote && hasRemoteHosts) {
|
||||
// biome-ignore lint/suspicious/noConsole: intentional
|
||||
console.error(
|
||||
`Collecting sessions from ${remoteHosts.length} homespace(s): ${remoteHosts.join(', ')}...`,
|
||||
)
|
||||
|
||||
@@ -160,7 +160,7 @@ function SetModelAndClose({
|
||||
// @[MODEL LAUNCH]: Update check for 1M access.
|
||||
if (model && isOpus1mUnavailable(model)) {
|
||||
onDone(
|
||||
`Opus 4.6 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`,
|
||||
`Opus 4.7 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
return
|
||||
|
||||
91
src/commands/summary/__tests__/summary.test.ts
Normal file
91
src/commands/summary/__tests__/summary.test.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { describe, test, expect, mock, beforeEach } from 'bun:test'
|
||||
|
||||
const mockManuallyExtract = mock(
|
||||
(): Promise<any> => Promise.resolve({ success: true }),
|
||||
)
|
||||
const mockGetContent = mock(
|
||||
(): Promise<any> => Promise.resolve('# Session Summary\n\nDid some work.'),
|
||||
)
|
||||
|
||||
mock.module(
|
||||
require.resolve('../../../services/SessionMemory/sessionMemory.js'),
|
||||
() => ({
|
||||
manuallyExtractSessionMemory: mockManuallyExtract,
|
||||
}),
|
||||
)
|
||||
mock.module(
|
||||
require.resolve('../../../services/SessionMemory/sessionMemoryUtils.js'),
|
||||
() => ({
|
||||
getSessionMemoryContent: mockGetContent,
|
||||
}),
|
||||
)
|
||||
|
||||
const { default: summaryCommand } = await import('../index.js')
|
||||
|
||||
const baseContext = {
|
||||
messages: [{ type: 'user', role: 'user', content: 'hello' }],
|
||||
options: { tools: [], mainLoopModel: 'test' },
|
||||
setMessages: () => {},
|
||||
onChangeAPIKey: () => {},
|
||||
} as any
|
||||
|
||||
async function callSummary(ctx = baseContext) {
|
||||
const mod = await summaryCommand.load()
|
||||
return mod.call('', ctx)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockManuallyExtract.mockReset()
|
||||
mockGetContent.mockReset()
|
||||
mockManuallyExtract.mockImplementation(() =>
|
||||
Promise.resolve({ success: true }),
|
||||
)
|
||||
mockGetContent.mockImplementation(() =>
|
||||
Promise.resolve('# Session Summary\n\nDid some work.'),
|
||||
)
|
||||
})
|
||||
|
||||
describe('summary command', () => {
|
||||
test('command metadata', () => {
|
||||
expect(summaryCommand.name).toBe('summary')
|
||||
expect(summaryCommand.type).toBe('local')
|
||||
expect(summaryCommand.isHidden).toBe(false)
|
||||
expect(typeof summaryCommand.load).toBe('function')
|
||||
})
|
||||
|
||||
test('refreshes and displays summary', async () => {
|
||||
const result = await callSummary()
|
||||
expect(result.type).toBe('text')
|
||||
expect((result as any).value).toContain('Session summary updated.')
|
||||
expect((result as any).value).toContain('Did some work.')
|
||||
expect(mockManuallyExtract).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test('handles extraction failure', async () => {
|
||||
mockManuallyExtract.mockImplementation(() =>
|
||||
Promise.resolve({ success: false, error: 'timeout' }),
|
||||
)
|
||||
const result = await callSummary()
|
||||
expect((result as any).value).toContain(
|
||||
'Failed to generate session summary',
|
||||
)
|
||||
expect((result as any).value).toContain('timeout')
|
||||
})
|
||||
|
||||
test('handles empty content after extraction', async () => {
|
||||
mockGetContent.mockImplementation(() => Promise.resolve(''))
|
||||
const result = await callSummary()
|
||||
expect((result as any).value).toContain('content is empty')
|
||||
})
|
||||
|
||||
test('handles null content after extraction', async () => {
|
||||
mockGetContent.mockImplementation(() => Promise.resolve(null))
|
||||
const result = await callSummary()
|
||||
expect((result as any).value).toContain('content is empty')
|
||||
})
|
||||
|
||||
test('handles no messages', async () => {
|
||||
const result = await callSummary({ ...baseContext, messages: [] })
|
||||
expect((result as any).value).toBe('No messages to summarize.')
|
||||
})
|
||||
})
|
||||
78
src/commands/summary/index.ts
Normal file
78
src/commands/summary/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* /summary — Generate and display a session summary.
|
||||
*
|
||||
* Triggers a manual Session Memory extraction (bypassing automatic thresholds),
|
||||
* then reads and displays the updated summary.md file.
|
||||
*/
|
||||
import type { Command, LocalCommandCall } from '../../types/command.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
|
||||
/** Only user/assistant/system messages are valid for API calls. */
|
||||
const API_SAFE_TYPES = new Set(['user', 'assistant', 'system'])
|
||||
|
||||
const call: LocalCommandCall = async (_args, context) => {
|
||||
const { messages } = context
|
||||
|
||||
// Filter to API-safe message types only.
|
||||
// context.messages includes progress/attachment/etc. that crash the API
|
||||
// call chain (normalizeMessagesForAPI → addCacheBreakpoints expects
|
||||
// only user/assistant). The automatic extraction path uses
|
||||
// createCacheSafeParams(REPLHookContext) which already has clean
|
||||
// messages; the manual path via /summary does not.
|
||||
const safeMessages = (messages ?? []).filter(
|
||||
(m): m is Message => m != null && API_SAFE_TYPES.has(m.type),
|
||||
)
|
||||
|
||||
if (safeMessages.length === 0) {
|
||||
return { type: 'text', value: 'No messages to summarize.' }
|
||||
}
|
||||
|
||||
try {
|
||||
const { manuallyExtractSessionMemory } = await import(
|
||||
'../../services/SessionMemory/sessionMemory.js'
|
||||
)
|
||||
const { getSessionMemoryContent } = await import(
|
||||
'../../services/SessionMemory/sessionMemoryUtils.js'
|
||||
)
|
||||
|
||||
const safeContext = { ...context, messages: safeMessages }
|
||||
const result = await manuallyExtractSessionMemory(safeMessages, safeContext)
|
||||
|
||||
if (!result.success) {
|
||||
return {
|
||||
type: 'text',
|
||||
value: `Failed to generate session summary: ${result.error ?? 'unknown error'}`,
|
||||
}
|
||||
}
|
||||
|
||||
const content = await getSessionMemoryContent()
|
||||
|
||||
if (!content || content.trim().length === 0) {
|
||||
return {
|
||||
type: 'text',
|
||||
value: 'Session summary was updated, but the content is empty.',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'text',
|
||||
value: `Session summary updated.\n\n${content}`,
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
type: 'text',
|
||||
value: `Failed to generate session summary: ${error instanceof Error ? error.message : String(error)}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const summary = {
|
||||
type: 'local',
|
||||
name: 'summary',
|
||||
description: 'Generate and display a session summary',
|
||||
supportsNonInteractive: true,
|
||||
isHidden: false,
|
||||
load: () => Promise.resolve({ call }),
|
||||
} satisfies Command
|
||||
|
||||
export default summary
|
||||
@@ -65,7 +65,7 @@ export function isUltraplanEnabled(): boolean {
|
||||
// load: the GrowthBook cache is empty at import and `/config` Gates can flip
|
||||
// it between invocations.
|
||||
function getUltraplanModel(): string {
|
||||
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_ultraplan_model', ALL_MODEL_CONFIGS.opus46.firstParty);
|
||||
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_ultraplan_model', ALL_MODEL_CONFIGS.opus47.firstParty);
|
||||
}
|
||||
|
||||
// prompt.txt is wrapped in <system-reminder> so the CCR browser hides
|
||||
|
||||
Reference in New Issue
Block a user