Skip to content

Commit 9da86b1

Browse files
committed
fix: address PR 27 review feedback
1 parent 498a691 commit 9da86b1

4 files changed

Lines changed: 210 additions & 9 deletions

File tree

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2+
3+
import { prepareApiConversationMessage } from "../apiConversationHistory.js"
4+
5+
describe("prepareApiConversationMessage", () => {
6+
beforeEach(() => {
7+
vi.useFakeTimers()
8+
vi.setSystemTime(new Date("2026-01-02T03:04:05.000Z"))
9+
})
10+
11+
afterEach(() => {
12+
vi.useRealTimers()
13+
})
14+
15+
it("prepends Anthropic thinking blocks with thought signatures", () => {
16+
const result = prepareApiConversationMessage({
17+
message: { role: "assistant", content: "answer" },
18+
reasoning: "private reasoning",
19+
api: {
20+
getResponseId: () => "response-1",
21+
getThoughtSignature: () => "signature-1",
22+
} as any,
23+
apiConfiguration: { apiProvider: "anthropic", apiModelId: "claude-3-5-sonnet" } as any,
24+
apiConversationHistory: [],
25+
}) as any
26+
27+
expect(result).toMatchObject({
28+
role: "assistant",
29+
id: "response-1",
30+
ts: Date.now(),
31+
})
32+
expect(result.content).toEqual([
33+
{ type: "thinking", thinking: "private reasoning", signature: "signature-1" },
34+
{ type: "text", text: "answer" },
35+
])
36+
})
37+
38+
it("prepends non-Anthropic reasoning blocks without dead summary fallback", () => {
39+
const result = prepareApiConversationMessage({
40+
message: { role: "assistant", content: "answer" },
41+
reasoning: "visible reasoning",
42+
api: {} as any,
43+
apiConfiguration: { apiProvider: "openrouter", openRouterModelId: "openai/gpt-4" } as any,
44+
apiConversationHistory: [],
45+
}) as any
46+
47+
expect(result.content).toEqual([
48+
{ type: "reasoning", text: "visible reasoning" },
49+
{ type: "text", text: "answer" },
50+
])
51+
expect(result.content[0]).not.toHaveProperty("summary")
52+
})
53+
54+
it("preserves encrypted reasoning content", () => {
55+
const result = prepareApiConversationMessage({
56+
message: { role: "assistant", content: [{ type: "text", text: "answer" }] },
57+
api: {
58+
getEncryptedContent: () => ({ encrypted_content: "encrypted", id: "reasoning-1" }),
59+
} as any,
60+
apiConfiguration: { apiProvider: "openrouter", openRouterModelId: "openai/gpt-4" } as any,
61+
apiConversationHistory: [],
62+
}) as any
63+
64+
expect(result.content).toEqual([
65+
{ type: "reasoning", summary: [], encrypted_content: "encrypted", id: "reasoning-1" },
66+
{ type: "text", text: "answer" },
67+
])
68+
})
69+
70+
it("appends thought signatures for non-Anthropic protocols", () => {
71+
const result = prepareApiConversationMessage({
72+
message: { role: "assistant", content: "answer" },
73+
api: {
74+
getThoughtSignature: () => "signature-1",
75+
getReasoningDetails: () => [{ type: "reasoning", text: "detail" }],
76+
} as any,
77+
apiConfiguration: { apiProvider: "openrouter", openRouterModelId: "openai/gpt-4" } as any,
78+
apiConversationHistory: [],
79+
}) as any
80+
81+
expect(result.reasoning_details).toEqual([{ type: "reasoning", text: "detail" }])
82+
expect(result.content).toEqual([
83+
{ type: "text", text: "answer" },
84+
{ type: "thoughtSignature", thoughtSignature: "signature-1" },
85+
])
86+
})
87+
88+
it("validates user tool_result blocks against the last effective assistant message", () => {
89+
const result = prepareApiConversationMessage({
90+
message: {
91+
role: "user",
92+
content: [{ type: "tool_result", tool_use_id: "wrong-id", content: "done" }],
93+
},
94+
api: {} as any,
95+
apiConfiguration: { apiProvider: "openrouter", openRouterModelId: "openai/gpt-4" } as any,
96+
apiConversationHistory: [
97+
{
98+
role: "assistant",
99+
content: [{ type: "tool_use", id: "tool-1", name: "read_file", input: {} }],
100+
} as any,
101+
],
102+
}) as any
103+
104+
expect(result.content).toEqual([{ type: "tool_result", tool_use_id: "tool-1", content: "done" }])
105+
expect(result.ts).toBe(Date.now())
106+
})
107+
})

src/core/task/apiConversationHistory.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ type ApiHistoryHandler = ApiHandler & {
1111
getResponseId?: () => string | undefined
1212
getEncryptedContent?: () => { encrypted_content: string; id?: string } | undefined
1313
getThoughtSignature?: () => string | undefined
14-
getSummary?: () => any[] | undefined
1514
getReasoningDetails?: () => any[] | undefined
1615
}
1716

@@ -46,7 +45,6 @@ function prepareAssistantMessage(
4645
const responseId = handler.getResponseId?.()
4746
const reasoningData = handler.getEncryptedContent?.()
4847
const thoughtSignature = handler.getThoughtSignature?.()
49-
const reasoningSummary = handler.getSummary?.()
5048
const reasoningDetails = handler.getReasoningDetails?.()
5149

5250
const modelId = getModelId(apiConfiguration)
@@ -79,7 +77,6 @@ function prepareAssistantMessage(
7977
const reasoningBlock = {
8078
type: "reasoning",
8179
text: reasoning,
82-
summary: reasoningSummary ?? ([] as any[]),
8380
}
8481

8582
prependContentBlock(messageWithTs, reasoningBlock)

src/core/webview/PendingEditOperationStore.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface PendingEditOperation {
99
}
1010

1111
export type PendingEditOperationInput = Omit<PendingEditOperation, "timeoutId" | "createdAt">
12+
export type PendingEditOperationView = Omit<PendingEditOperation, "timeoutId" | "createdAt">
1213

1314
export class PendingEditOperationStore {
1415
private readonly operations = new Map<string, PendingEditOperation>()
@@ -23,7 +24,7 @@ export class PendingEditOperationStore {
2324

2425
const timeoutId = setTimeout(() => {
2526
this.clear(operationId)
26-
this.log(`[setPendingEditOperation] Automatically cleared stale pending operation: ${operationId}`)
27+
this.log(`[PendingEditOperationStore.set] Automatically cleared stale pending operation: ${operationId}`)
2728
}, this.timeoutMs)
2829

2930
this.operations.set(operationId, {
@@ -32,11 +33,22 @@ export class PendingEditOperationStore {
3233
createdAt: Date.now(),
3334
})
3435

35-
this.log(`[setPendingEditOperation] Set pending operation: ${operationId}`)
36+
this.log(`[PendingEditOperationStore.set] Set pending operation: ${operationId}`)
3637
}
3738

38-
get(operationId: string): PendingEditOperation | undefined {
39-
return this.operations.get(operationId)
39+
get(operationId: string): PendingEditOperationView | undefined {
40+
const operation = this.operations.get(operationId)
41+
if (!operation) {
42+
return undefined
43+
}
44+
45+
return {
46+
messageTs: operation.messageTs,
47+
editedContent: operation.editedContent,
48+
images: operation.images,
49+
messageIndex: operation.messageIndex,
50+
apiConversationHistoryIndex: operation.apiConversationHistoryIndex,
51+
}
4052
}
4153

4254
clear(operationId: string): boolean {
@@ -47,7 +59,7 @@ export class PendingEditOperationStore {
4759

4860
clearTimeout(operation.timeoutId)
4961
this.operations.delete(operationId)
50-
this.log(`[clearPendingEditOperation] Cleared pending operation: ${operationId}`)
62+
this.log(`[PendingEditOperationStore.clear] Cleared pending operation: ${operationId}`)
5163
return true
5264
}
5365

@@ -57,6 +69,6 @@ export class PendingEditOperationStore {
5769
}
5870

5971
this.operations.clear()
60-
this.log("[clearAllPendingEditOperations] Cleared all pending operations")
72+
this.log("[PendingEditOperationStore.clearAll] Cleared all pending operations")
6173
}
6274
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2+
3+
import { PendingEditOperationStore, type PendingEditOperationInput } from "../PendingEditOperationStore.js"
4+
5+
describe("PendingEditOperationStore", () => {
6+
const editData: PendingEditOperationInput = {
7+
messageTs: 123,
8+
editedContent: "edited",
9+
images: ["image.png"],
10+
messageIndex: 1,
11+
apiConversationHistoryIndex: 2,
12+
}
13+
14+
let log: ReturnType<typeof vi.fn>
15+
let store: PendingEditOperationStore
16+
17+
beforeEach(() => {
18+
vi.useFakeTimers()
19+
log = vi.fn()
20+
store = new PendingEditOperationStore(1_000, log)
21+
})
22+
23+
afterEach(() => {
24+
vi.useRealTimers()
25+
})
26+
27+
it("clears a prior operation with the same id", () => {
28+
store.set("op", editData)
29+
vi.advanceTimersByTime(500)
30+
31+
store.set("op", { ...editData, editedContent: "replacement" })
32+
vi.advanceTimersByTime(500)
33+
34+
expect(store.get("op")).toMatchObject({ editedContent: "replacement" })
35+
expect(log).toHaveBeenCalledWith("[PendingEditOperationStore.clear] Cleared pending operation: op")
36+
37+
vi.advanceTimersByTime(500)
38+
39+
expect(store.get("op")).toBeUndefined()
40+
})
41+
42+
it("returns false on clear miss and true on hit", () => {
43+
expect(store.clear("missing")).toBe(false)
44+
45+
store.set("op", editData)
46+
47+
expect(store.clear("op")).toBe(true)
48+
expect(store.get("op")).toBeUndefined()
49+
})
50+
51+
it("hides timer metadata from get results", () => {
52+
store.set("op", editData)
53+
54+
const operation = store.get("op")
55+
56+
expect(operation).toEqual(editData)
57+
expect(operation).not.toHaveProperty("timeoutId")
58+
expect(operation).not.toHaveProperty("createdAt")
59+
})
60+
61+
it("auto-clears after timeoutMs and logs", () => {
62+
store.set("op", editData)
63+
64+
vi.advanceTimersByTime(1_000)
65+
66+
expect(store.get("op")).toBeUndefined()
67+
expect(log).toHaveBeenCalledWith(
68+
"[PendingEditOperationStore.set] Automatically cleared stale pending operation: op",
69+
)
70+
})
71+
72+
it("clearAll clears timers without later auto-clear logs", () => {
73+
store.set("op-1", editData)
74+
store.set("op-2", { ...editData, editedContent: "second" })
75+
76+
store.clearAll()
77+
log.mockClear()
78+
79+
vi.advanceTimersByTime(1_000)
80+
81+
expect(store.get("op-1")).toBeUndefined()
82+
expect(store.get("op-2")).toBeUndefined()
83+
expect(log).not.toHaveBeenCalled()
84+
})
85+
})

0 commit comments

Comments
 (0)