mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Merge compact state fix into local main
Bring the desktop compact state repair from the validation worktree into local main after focused, full local, packaged-app, and coverage checks. Constraint: Local main was clean and shared the same base commit as the validation worktree. Confidence: high Scope-risk: moderate Directive: Keep compact status null handling and tail compact-card cleanup together. Tested: bun run check:desktop Tested: bun run check:server Tested: bun run check:policy Tested: bun run check:native Tested: bun run check:coverage Tested: SKIP_INSTALL=1 PRESERVE_TAURI_TARGET=1 ./desktop/scripts/build-macos-arm64.sh Not-tested: Full live auto-compact threshold trigger in packaged app; Tauri sidecar did not inherit CLAUDE_AUTOCOMPACT_PCT_OVERRIDE during manual smoke. Related: cb922b01
This commit is contained in:
commit
4918cd8032
@ -223,7 +223,7 @@ describe('chatStore history mapping', () => {
|
||||
expect(mapped[3]).toMatchObject({ parentToolUseId: 'agent-1' })
|
||||
})
|
||||
|
||||
it('maps compact boundary and summary history into one compact card', () => {
|
||||
it('maps compact boundary and summary history without hiding pre-compact messages', () => {
|
||||
const messages: MessageEntry[] = [
|
||||
{
|
||||
id: 'old-user',
|
||||
@ -259,8 +259,18 @@ describe('chatStore history mapping', () => {
|
||||
|
||||
const mapped = mapHistoryMessagesToUiMessages(messages)
|
||||
|
||||
expect(mapped).toHaveLength(1)
|
||||
expect(mapped).toHaveLength(3)
|
||||
expect(mapped).toMatchObject([
|
||||
{
|
||||
id: 'old-user',
|
||||
type: 'user_text',
|
||||
content: 'Build the billing import flow',
|
||||
},
|
||||
{
|
||||
id: 'old-assistant',
|
||||
type: 'assistant_text',
|
||||
content: 'Implemented the flow.',
|
||||
},
|
||||
{
|
||||
type: 'compact_summary',
|
||||
title: 'Context compacted',
|
||||
@ -1920,8 +1930,10 @@ describe('chatStore history mapping', () => {
|
||||
})
|
||||
|
||||
const messages = useChatStore.getState().sessions[TEST_SESSION_ID]?.messages ?? []
|
||||
expect(messages).toHaveLength(1)
|
||||
expect(messages).toHaveLength(3)
|
||||
expect(messages).toMatchObject([
|
||||
{ id: 'old-user', type: 'user_text', content: 'Build the billing import flow' },
|
||||
{ id: 'old-assistant', type: 'assistant_text', content: 'Implemented the flow.' },
|
||||
{
|
||||
type: 'compact_summary',
|
||||
title: 'Context compacted',
|
||||
@ -1959,6 +1971,7 @@ describe('chatStore history mapping', () => {
|
||||
type: 'system_notification',
|
||||
subtype: 'compact_boundary',
|
||||
message: 'Context compacted',
|
||||
data: { trigger: 'manual' },
|
||||
})
|
||||
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||
type: 'system_notification',
|
||||
@ -1976,6 +1989,7 @@ describe('chatStore history mapping', () => {
|
||||
expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toMatchObject([
|
||||
{
|
||||
type: 'compact_summary',
|
||||
trigger: 'manual',
|
||||
summary: 'Implemented the billing report and verified export behavior.',
|
||||
},
|
||||
])
|
||||
@ -2016,6 +2030,11 @@ describe('chatStore history mapping', () => {
|
||||
expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.chatState).toBe('compacting')
|
||||
expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.statusVerb).toBe('Compacting conversation')
|
||||
expect(useChatStore.getState().sessions[TEST_SESSION_ID]?.messages).toMatchObject([
|
||||
{
|
||||
id: 'old-user',
|
||||
type: 'user_text',
|
||||
content: 'old context',
|
||||
},
|
||||
{
|
||||
type: 'compact_summary',
|
||||
phase: 'compacting',
|
||||
@ -2024,6 +2043,110 @@ describe('chatStore history mapping', () => {
|
||||
expect(updateTabStatusMock).toHaveBeenLastCalledWith(TEST_SESSION_ID, 'running')
|
||||
})
|
||||
|
||||
it('removes the transient compacting card when compaction is canceled', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[TEST_SESSION_ID]: {
|
||||
messages: [
|
||||
{ id: 'old-user', type: 'user_text', content: 'old context', timestamp: 1 },
|
||||
],
|
||||
chatState: 'thinking',
|
||||
connectionState: 'connected',
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
pendingComputerUsePermission: null,
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
elapsedSeconds: 0,
|
||||
statusVerb: '',
|
||||
slashCommands: [],
|
||||
agentTaskNotifications: {},
|
||||
elapsedTimer: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||
type: 'status',
|
||||
state: 'compacting',
|
||||
verb: 'Compacting conversation',
|
||||
})
|
||||
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||
type: 'error',
|
||||
message: 'Compaction canceled.',
|
||||
code: 'aborted',
|
||||
})
|
||||
|
||||
const session = useChatStore.getState().sessions[TEST_SESSION_ID]
|
||||
expect(session?.statusVerb).toBe('')
|
||||
expect(session?.messages).toMatchObject([
|
||||
{
|
||||
id: 'old-user',
|
||||
type: 'user_text',
|
||||
content: 'old context',
|
||||
},
|
||||
{
|
||||
type: 'error',
|
||||
message: 'Compaction canceled.',
|
||||
},
|
||||
])
|
||||
expect(session?.messages.some((message) => message.type === 'compact_summary' && message.phase === 'compacting')).toBe(false)
|
||||
expect(updateTabStatusMock).toHaveBeenLastCalledWith(TEST_SESSION_ID, 'error')
|
||||
})
|
||||
|
||||
it('removes the transient compacting card when compacting status ends without a boundary', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
[TEST_SESSION_ID]: {
|
||||
messages: [
|
||||
{ id: 'old-user', type: 'user_text', content: 'old context', timestamp: 1 },
|
||||
],
|
||||
chatState: 'thinking',
|
||||
connectionState: 'connected',
|
||||
streamingText: '',
|
||||
streamingToolInput: '',
|
||||
activeToolUseId: null,
|
||||
activeToolName: null,
|
||||
activeThinkingId: null,
|
||||
pendingPermission: null,
|
||||
pendingComputerUsePermission: null,
|
||||
tokenUsage: { input_tokens: 0, output_tokens: 0 },
|
||||
elapsedSeconds: 0,
|
||||
statusVerb: '',
|
||||
slashCommands: [],
|
||||
agentTaskNotifications: {},
|
||||
elapsedTimer: null,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||
type: 'status',
|
||||
state: 'compacting',
|
||||
verb: 'Compacting conversation',
|
||||
})
|
||||
useChatStore.getState().handleServerMessage(TEST_SESSION_ID, {
|
||||
type: 'status',
|
||||
state: 'thinking',
|
||||
verb: 'Thinking',
|
||||
})
|
||||
|
||||
const session = useChatStore.getState().sessions[TEST_SESSION_ID]
|
||||
expect(session?.chatState).toBe('thinking')
|
||||
expect(session?.messages).toMatchObject([
|
||||
{
|
||||
id: 'old-user',
|
||||
type: 'user_text',
|
||||
content: 'old context',
|
||||
},
|
||||
])
|
||||
expect(session?.messages.some((message) => message.type === 'compact_summary' && message.phase === 'compacting')).toBe(false)
|
||||
expect(updateTabStatusMock).toHaveBeenLastCalledWith(TEST_SESSION_ID, 'running')
|
||||
})
|
||||
|
||||
it('tracks API retry status until the request finishes', () => {
|
||||
useChatStore.setState({
|
||||
sessions: {
|
||||
|
||||
@ -322,20 +322,15 @@ function compactMetadataFromUnknown(data: unknown): Pick<CompactSummaryMessage,
|
||||
}
|
||||
}
|
||||
|
||||
function appendOrUpdateCompactSummary(
|
||||
function appendOrUpdateTailCompactSummary(
|
||||
messages: UIMessage[],
|
||||
update: Partial<Omit<CompactSummaryMessage, 'id' | 'type' | 'timestamp'>>,
|
||||
timestamp: number,
|
||||
): UIMessage[] {
|
||||
let existingIndex = -1
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
if (messages[index]?.type === 'compact_summary') {
|
||||
existingIndex = index
|
||||
break
|
||||
}
|
||||
}
|
||||
if (existingIndex >= 0) {
|
||||
const existing = messages[existingIndex] as CompactSummaryMessage
|
||||
const existingIndex = messages.length - 1
|
||||
const existingMessage = messages[existingIndex]
|
||||
if (existingMessage?.type === 'compact_summary') {
|
||||
const existing = existingMessage
|
||||
const next: CompactSummaryMessage = {
|
||||
...existing,
|
||||
...update,
|
||||
@ -361,13 +356,12 @@ function appendOrUpdateCompactSummary(
|
||||
]
|
||||
}
|
||||
|
||||
function collapseToCompactSummary(
|
||||
messages: UIMessage[],
|
||||
update: Partial<Omit<CompactSummaryMessage, 'id' | 'type' | 'timestamp'>>,
|
||||
timestamp: number,
|
||||
): UIMessage[] {
|
||||
const existing = [...messages].reverse().find((message): message is CompactSummaryMessage => message.type === 'compact_summary')
|
||||
return appendOrUpdateCompactSummary(existing ? [existing] : [], update, timestamp)
|
||||
function dropTailCompactingCompactSummary(messages: UIMessage[]): UIMessage[] {
|
||||
const tail = messages[messages.length - 1]
|
||||
if (tail?.type === 'compact_summary' && tail.phase === 'compacting') {
|
||||
return messages.slice(0, -1)
|
||||
}
|
||||
return messages
|
||||
}
|
||||
|
||||
function upsertBackgroundTaskMessage(
|
||||
@ -1055,7 +1049,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
nextMessages = appendAssistantTextMessage(nextMessages, pendingText, Date.now())
|
||||
}
|
||||
if (msg.state === 'compacting') {
|
||||
nextMessages = collapseToCompactSummary(
|
||||
nextMessages = appendOrUpdateTailCompactSummary(
|
||||
nextMessages,
|
||||
{
|
||||
title: 'Context compacted',
|
||||
@ -1063,6 +1057,8 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
},
|
||||
Date.now(),
|
||||
)
|
||||
} else {
|
||||
nextMessages = dropTailCompactingCompactSummary(nextMessages)
|
||||
}
|
||||
return {
|
||||
chatState: preserveStreamingTurn ? 'streaming' : msg.state,
|
||||
@ -1074,7 +1070,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
...(msg.tokens ? { tokenUsage: { ...session.tokenUsage, output_tokens: msg.tokens } } : {}),
|
||||
...(msg.state === 'idle' ? { activeThinkingId: null } : {}),
|
||||
...(msg.state === 'idle' ? { apiRetry: null } : {}),
|
||||
...((shouldFlush || msg.state === 'compacting') ? { messages: nextMessages } : {}),
|
||||
...(nextMessages !== session.messages ? { messages: nextMessages } : {}),
|
||||
...(shouldFlush ? {
|
||||
streamingText: '',
|
||||
} : pendingText !== session.streamingText ? { streamingText: pendingText } : {}),
|
||||
@ -1348,12 +1344,14 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
if (pendingText.trim()) {
|
||||
newMessages = appendAssistantTextMessage(newMessages, pendingText, Date.now())
|
||||
}
|
||||
newMessages = dropTailCompactingCompactSummary(newMessages)
|
||||
newMessages = [...newMessages, { id: nextId(), type: 'error', message: msg.message, code: msg.code, timestamp: Date.now() }]
|
||||
return {
|
||||
messages: newMessages,
|
||||
chatState: 'idle',
|
||||
activeThinkingId: null,
|
||||
streamingText: '',
|
||||
statusVerb: '',
|
||||
pendingPermission: null,
|
||||
pendingComputerUsePermission: null,
|
||||
apiRetry: null,
|
||||
@ -1439,7 +1437,7 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
update((session) => ({
|
||||
chatState: session.chatState === 'compacting' ? 'thinking' : session.chatState,
|
||||
statusVerb: session.chatState === 'compacting' ? '' : session.statusVerb,
|
||||
messages: collapseToCompactSummary(
|
||||
messages: appendOrUpdateTailCompactSummary(
|
||||
session.messages,
|
||||
{
|
||||
title: typeof msg.message === 'string' && msg.message.trim()
|
||||
@ -1456,13 +1454,12 @@ export const useChatStore = create<ChatStore>((set, get) => ({
|
||||
const summary = extractCompactSummaryContent(msg.message)
|
||||
if (summary) {
|
||||
update((session) => ({
|
||||
messages: collapseToCompactSummary(
|
||||
messages: appendOrUpdateTailCompactSummary(
|
||||
session.messages,
|
||||
{
|
||||
title: 'Context compacted',
|
||||
phase: 'complete',
|
||||
summary,
|
||||
trigger: 'auto',
|
||||
...compactMetadataFromUnknown(msg.data),
|
||||
},
|
||||
Date.now(),
|
||||
@ -2198,7 +2195,7 @@ export function mapHistoryMessagesToUiMessages(
|
||||
const timestamp = new Date(msg.timestamp).getTime()
|
||||
if (msg.type === 'system' && typeof msg.content === 'string') {
|
||||
if (msg.content.trim() === 'Conversation compacted' || msg.content.trim() === 'Context compacted') {
|
||||
const compactMessages = collapseToCompactSummary(
|
||||
const compactMessages = appendOrUpdateTailCompactSummary(
|
||||
uiMessages,
|
||||
{ title: 'Context compacted', phase: 'complete' },
|
||||
timestamp,
|
||||
@ -2244,13 +2241,12 @@ export function mapHistoryMessagesToUiMessages(
|
||||
|
||||
const compactSummary = extractCompactSummaryContent(msg.content)
|
||||
if (compactSummary) {
|
||||
const compactMessages = collapseToCompactSummary(
|
||||
const compactMessages = appendOrUpdateTailCompactSummary(
|
||||
uiMessages,
|
||||
{
|
||||
title: 'Context compacted',
|
||||
phase: 'complete',
|
||||
summary: compactSummary,
|
||||
trigger: 'auto',
|
||||
},
|
||||
timestamp,
|
||||
)
|
||||
|
||||
@ -57,6 +57,18 @@ describe('WebSocket compact events', () => {
|
||||
type: 'system',
|
||||
subtype: 'status',
|
||||
status: null,
|
||||
}, 'session-1')).toEqual([
|
||||
{
|
||||
type: 'status',
|
||||
state: 'thinking',
|
||||
verb: 'Thinking',
|
||||
},
|
||||
])
|
||||
|
||||
expect(translateCliMessage({
|
||||
type: 'system',
|
||||
subtype: 'status',
|
||||
status: 'warming',
|
||||
}, 'session-1')).toEqual([])
|
||||
})
|
||||
|
||||
|
||||
@ -1284,6 +1284,9 @@ export function translateCliMessage(cliMsg: any, sessionId: string): ServerMessa
|
||||
verb: 'Compacting conversation',
|
||||
}]
|
||||
}
|
||||
if (cliMsg.status == null) {
|
||||
return [{ type: 'status', state: 'thinking', verb: 'Thinking' }]
|
||||
}
|
||||
return []
|
||||
}
|
||||
if (subtype === 'hook_started' || subtype === 'hook_response') {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user