kanna-code 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/index-C952_xJD.css +32 -0
- package/dist/client/assets/index-lgfU0zw2.js +606 -0
- package/dist/client/chat-sounds/Blow.mp3 +0 -0
- package/dist/client/chat-sounds/Bottle.mp3 +0 -0
- package/dist/client/chat-sounds/Frog.mp3 +0 -0
- package/dist/client/chat-sounds/Funk.mp3 +0 -0
- package/dist/client/chat-sounds/Glass.mp3 +0 -0
- package/dist/client/chat-sounds/Ping.mp3 +0 -0
- package/dist/client/chat-sounds/Pop.mp3 +0 -0
- package/dist/client/chat-sounds/Purr.mp3 +0 -0
- package/dist/client/chat-sounds/Tink.mp3 +0 -0
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/agent.test.ts +89 -2
- package/src/server/agent.ts +31 -12
- package/src/server/event-store.test.ts +65 -0
- package/src/server/event-store.ts +27 -1
- package/src/server/events.ts +8 -0
- package/src/server/generate-title.ts +42 -2
- package/src/server/quick-response.test.ts +126 -3
- package/src/server/quick-response.ts +107 -14
- package/src/server/read-models.test.ts +4 -0
- package/src/server/read-models.ts +1 -0
- package/src/server/title-generation.live.test.ts +44 -0
- package/src/server/ws-router.test.ts +181 -0
- package/src/server/ws-router.ts +18 -0
- package/src/shared/protocol.ts +1 -0
- package/src/shared/types.ts +1 -0
- package/dist/client/assets/index-BvhI1JYs.css +0 -32
- package/dist/client/assets/index-CLqK0AWC.js +0 -606
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { query } from "@anthropic-ai/claude-agent-sdk"
|
|
2
|
+
import { homedir } from "node:os"
|
|
3
|
+
import { getDataRootDir } from "../shared/branding"
|
|
2
4
|
import { CodexAppServerManager } from "./codex-app-server"
|
|
3
5
|
|
|
4
6
|
const CLAUDE_STRUCTURED_TIMEOUT_MS = 5_000
|
|
@@ -24,6 +26,20 @@ interface QuickResponseAdapterArgs {
|
|
|
24
26
|
runCodexStructured?: (args: Omit<StructuredQuickResponseArgs<unknown>, "parse">) => Promise<unknown | null>
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
export interface StructuredQuickResponseFailure {
|
|
30
|
+
provider: "claude" | "codex"
|
|
31
|
+
reason: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StructuredQuickResponseResult<T> {
|
|
35
|
+
value: T | null
|
|
36
|
+
failures: StructuredQuickResponseFailure[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getQuickResponseWorkspace(env: Record<string, string | undefined> = process.env) {
|
|
40
|
+
return getDataRootDir(homedir(), env)
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
function parseJsonText(value: string): unknown | null {
|
|
28
44
|
const trimmed = value.trim()
|
|
29
45
|
if (!trimmed) return null
|
|
@@ -45,10 +61,35 @@ function parseJsonText(value: string): unknown | null {
|
|
|
45
61
|
return null
|
|
46
62
|
}
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
function structuredOutputFromSdkMessage(message: unknown): unknown | null {
|
|
65
|
+
if (!message || typeof message !== "object") return null
|
|
66
|
+
|
|
67
|
+
const record = message as Record<string, unknown>
|
|
68
|
+
if (record.type === "result") {
|
|
69
|
+
return record.structured_output ?? null
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const assistantMessage = record.message
|
|
73
|
+
if (!assistantMessage || typeof assistantMessage !== "object") return null
|
|
74
|
+
const content = (assistantMessage as { content?: unknown }).content
|
|
75
|
+
if (!Array.isArray(content)) return null
|
|
76
|
+
|
|
77
|
+
for (const item of content) {
|
|
78
|
+
if (!item || typeof item !== "object") continue
|
|
79
|
+
const toolUse = item as Record<string, unknown>
|
|
80
|
+
if (toolUse.type === "tool_use" && toolUse.name === "StructuredOutput") {
|
|
81
|
+
return toolUse.input ?? null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function runClaudeStructured(args: Omit<StructuredQuickResponseArgs<unknown>, "parse">): Promise<unknown | null> {
|
|
49
89
|
const q = query({
|
|
50
90
|
prompt: args.prompt,
|
|
51
91
|
options: {
|
|
92
|
+
cwd: args.cwd,
|
|
52
93
|
model: "haiku",
|
|
53
94
|
tools: [],
|
|
54
95
|
systemPrompt: "",
|
|
@@ -66,8 +107,9 @@ async function runClaudeStructured(args: Omit<StructuredQuickResponseArgs<unknow
|
|
|
66
107
|
const result = await Promise.race<unknown | null>([
|
|
67
108
|
(async () => {
|
|
68
109
|
for await (const message of q) {
|
|
69
|
-
|
|
70
|
-
|
|
110
|
+
const structuredOutput = structuredOutputFromSdkMessage(message)
|
|
111
|
+
if (structuredOutput !== null) {
|
|
112
|
+
return structuredOutput
|
|
71
113
|
}
|
|
72
114
|
}
|
|
73
115
|
return null
|
|
@@ -91,12 +133,13 @@ async function runClaudeStructured(args: Omit<StructuredQuickResponseArgs<unknow
|
|
|
91
133
|
}
|
|
92
134
|
}
|
|
93
135
|
|
|
94
|
-
async function runCodexStructured(
|
|
136
|
+
export async function runCodexStructured(
|
|
95
137
|
codexManager: CodexAppServerManager,
|
|
96
138
|
args: Omit<StructuredQuickResponseArgs<unknown>, "parse">
|
|
97
139
|
): Promise<unknown | null> {
|
|
98
140
|
const response = await codexManager.generateStructured({
|
|
99
141
|
cwd: args.cwd,
|
|
142
|
+
model: "gpt-5.4-mini",
|
|
100
143
|
prompt: `${args.prompt}\n\nReturn JSON only that matches this schema:\n${JSON.stringify(args.schema, null, 2)}`,
|
|
101
144
|
})
|
|
102
145
|
if (typeof response !== "string") return null
|
|
@@ -115,17 +158,45 @@ export class QuickResponseAdapter {
|
|
|
115
158
|
runCodexStructured(this.codexManager, structuredArgs))
|
|
116
159
|
}
|
|
117
160
|
async generateStructured<T>(args: StructuredQuickResponseArgs<T>): Promise<T | null> {
|
|
161
|
+
const result = await this.generateStructuredWithDiagnostics(args)
|
|
162
|
+
return result.value
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async generateStructuredWithDiagnostics<T>(args: StructuredQuickResponseArgs<T>): Promise<StructuredQuickResponseResult<T>> {
|
|
118
166
|
const request = {
|
|
119
|
-
cwd:
|
|
167
|
+
cwd: getQuickResponseWorkspace(),
|
|
120
168
|
task: args.task,
|
|
121
169
|
prompt: args.prompt,
|
|
122
170
|
schema: args.schema,
|
|
123
171
|
}
|
|
124
172
|
|
|
173
|
+
const failures: StructuredQuickResponseFailure[] = []
|
|
125
174
|
const claudeResult = await this.tryProvider("claude", args.task, args.parse, () => this.runClaudeStructured(request))
|
|
126
|
-
if (claudeResult !== null)
|
|
175
|
+
if (claudeResult.value !== null) {
|
|
176
|
+
return {
|
|
177
|
+
value: claudeResult.value,
|
|
178
|
+
failures,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (claudeResult.failure) {
|
|
182
|
+
failures.push(claudeResult.failure)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const codexResult = await this.tryProvider("codex", args.task, args.parse, () => this.runCodexStructured(request))
|
|
186
|
+
if (codexResult.value !== null) {
|
|
187
|
+
return {
|
|
188
|
+
value: codexResult.value,
|
|
189
|
+
failures,
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (codexResult.failure) {
|
|
193
|
+
failures.push(codexResult.failure)
|
|
194
|
+
}
|
|
127
195
|
|
|
128
|
-
return
|
|
196
|
+
return {
|
|
197
|
+
value: null,
|
|
198
|
+
failures,
|
|
199
|
+
}
|
|
129
200
|
}
|
|
130
201
|
|
|
131
202
|
private async tryProvider<T>(
|
|
@@ -133,21 +204,43 @@ export class QuickResponseAdapter {
|
|
|
133
204
|
task: string,
|
|
134
205
|
parse: (value: unknown) => T | null,
|
|
135
206
|
run: () => Promise<unknown | null>
|
|
136
|
-
): Promise<T | null> {
|
|
207
|
+
): Promise<{ value: T | null; failure: StructuredQuickResponseFailure | null }> {
|
|
137
208
|
try {
|
|
138
209
|
const result = await run()
|
|
139
210
|
if (result === null) {
|
|
140
|
-
return
|
|
211
|
+
return {
|
|
212
|
+
value: null,
|
|
213
|
+
failure: {
|
|
214
|
+
provider,
|
|
215
|
+
reason: `${provider} returned no result for ${task}`,
|
|
216
|
+
},
|
|
217
|
+
}
|
|
141
218
|
}
|
|
142
|
-
|
|
219
|
+
|
|
143
220
|
const parsed = parse(result)
|
|
144
221
|
if (parsed === null) {
|
|
145
|
-
return
|
|
222
|
+
return {
|
|
223
|
+
value: null,
|
|
224
|
+
failure: {
|
|
225
|
+
provider,
|
|
226
|
+
reason: `${provider} returned invalid structured output for ${task}`,
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
value: parsed,
|
|
233
|
+
failure: null,
|
|
146
234
|
}
|
|
147
|
-
|
|
148
|
-
return parsed
|
|
149
235
|
} catch (error) {
|
|
150
|
-
|
|
236
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
237
|
+
return {
|
|
238
|
+
value: null,
|
|
239
|
+
failure: {
|
|
240
|
+
provider,
|
|
241
|
+
reason: `${provider} failed ${task}: ${message}`,
|
|
242
|
+
},
|
|
243
|
+
}
|
|
151
244
|
}
|
|
152
245
|
}
|
|
153
246
|
}
|
|
@@ -19,6 +19,7 @@ describe("read models", () => {
|
|
|
19
19
|
title: "Chat",
|
|
20
20
|
createdAt: 1,
|
|
21
21
|
updatedAt: 1,
|
|
22
|
+
unread: true,
|
|
22
23
|
provider: "codex",
|
|
23
24
|
planMode: false,
|
|
24
25
|
sessionToken: "thread-1",
|
|
@@ -27,6 +28,7 @@ describe("read models", () => {
|
|
|
27
28
|
|
|
28
29
|
const sidebar = deriveSidebarData(state, new Map())
|
|
29
30
|
expect(sidebar.projectGroups[0]?.chats[0]?.provider).toBe("codex")
|
|
31
|
+
expect(sidebar.projectGroups[0]?.chats[0]?.unread).toBe(true)
|
|
30
32
|
})
|
|
31
33
|
|
|
32
34
|
test("includes available providers in chat snapshots", () => {
|
|
@@ -45,6 +47,7 @@ describe("read models", () => {
|
|
|
45
47
|
title: "Chat",
|
|
46
48
|
createdAt: 1,
|
|
47
49
|
updatedAt: 1,
|
|
50
|
+
unread: false,
|
|
48
51
|
provider: "claude",
|
|
49
52
|
planMode: true,
|
|
50
53
|
sessionToken: "session-1",
|
|
@@ -77,6 +80,7 @@ describe("read models", () => {
|
|
|
77
80
|
title: "Chat",
|
|
78
81
|
createdAt: 1,
|
|
79
82
|
updatedAt: 75,
|
|
83
|
+
unread: false,
|
|
80
84
|
provider: "codex",
|
|
81
85
|
planMode: false,
|
|
82
86
|
sessionToken: null,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { CodexAppServerManager } from "./codex-app-server"
|
|
3
|
+
import { fallbackTitleFromMessage, generateTitleForChatDetailed } from "./generate-title"
|
|
4
|
+
import { QuickResponseAdapter, runClaudeStructured, runCodexStructured } from "./quick-response"
|
|
5
|
+
|
|
6
|
+
const shouldRunLiveTests = process.env.KANNA_RUN_LIVE_TITLE_TESTS === "1"
|
|
7
|
+
const LIVE_MESSAGE = "Please help me debug a websocket reconnection issue in a Bun server app"
|
|
8
|
+
|
|
9
|
+
if (shouldRunLiveTests) {
|
|
10
|
+
describe("live title generation", () => {
|
|
11
|
+
test("generates a title with Claude", async () => {
|
|
12
|
+
const adapter = new QuickResponseAdapter({
|
|
13
|
+
runClaudeStructured,
|
|
14
|
+
runCodexStructured: async () => {
|
|
15
|
+
throw new Error("Codex fallback should not be used in the Claude live title test")
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
const result = await generateTitleForChatDetailed(LIVE_MESSAGE, process.cwd(), adapter)
|
|
20
|
+
|
|
21
|
+
expect(result.usedFallback).toBe(false)
|
|
22
|
+
expect(result.failureMessage).toBeNull()
|
|
23
|
+
expect(typeof result.title).toBe("string")
|
|
24
|
+
expect(result.title).not.toBe(fallbackTitleFromMessage(LIVE_MESSAGE))
|
|
25
|
+
}, 15_000)
|
|
26
|
+
|
|
27
|
+
test("generates a title with Codex", async () => {
|
|
28
|
+
const codexManager = new CodexAppServerManager()
|
|
29
|
+
const adapter = new QuickResponseAdapter({
|
|
30
|
+
runClaudeStructured: async () => {
|
|
31
|
+
throw new Error("Claude should not be used in the Codex live title test")
|
|
32
|
+
},
|
|
33
|
+
runCodexStructured: async (args) => runCodexStructured(codexManager, args),
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const result = await generateTitleForChatDetailed(LIVE_MESSAGE, process.cwd(), adapter)
|
|
37
|
+
|
|
38
|
+
expect(result.usedFallback).toBe(false)
|
|
39
|
+
expect(result.failureMessage).toBeNull()
|
|
40
|
+
expect(typeof result.title).toBe("string")
|
|
41
|
+
expect(result.title).not.toBe(fallbackTitleFromMessage(LIVE_MESSAGE))
|
|
42
|
+
}, 15_000)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
@@ -176,6 +176,187 @@ describe("ws-router", () => {
|
|
|
176
176
|
})
|
|
177
177
|
})
|
|
178
178
|
|
|
179
|
+
test("marks chats read and rebroadcasts sidebar snapshots", async () => {
|
|
180
|
+
const state = createEmptyState()
|
|
181
|
+
state.projectsById.set("project-1", {
|
|
182
|
+
id: "project-1",
|
|
183
|
+
localPath: "/tmp/project",
|
|
184
|
+
title: "Project",
|
|
185
|
+
createdAt: 1,
|
|
186
|
+
updatedAt: 1,
|
|
187
|
+
})
|
|
188
|
+
state.projectIdsByPath.set("/tmp/project", "project-1")
|
|
189
|
+
state.chatsById.set("chat-1", {
|
|
190
|
+
id: "chat-1",
|
|
191
|
+
projectId: "project-1",
|
|
192
|
+
title: "Chat",
|
|
193
|
+
createdAt: 1,
|
|
194
|
+
updatedAt: 1,
|
|
195
|
+
unread: true,
|
|
196
|
+
provider: null,
|
|
197
|
+
planMode: false,
|
|
198
|
+
sessionToken: null,
|
|
199
|
+
lastTurnOutcome: null,
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const store = {
|
|
203
|
+
state,
|
|
204
|
+
async setChatReadState(chatId: string, unread: boolean) {
|
|
205
|
+
const chat = state.chatsById.get(chatId)
|
|
206
|
+
if (!chat) throw new Error("Chat not found")
|
|
207
|
+
chat.unread = unread
|
|
208
|
+
},
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const router = createWsRouter({
|
|
212
|
+
store: store as never,
|
|
213
|
+
agent: { getActiveStatuses: () => new Map() } as never,
|
|
214
|
+
terminals: {
|
|
215
|
+
getSnapshot: () => null,
|
|
216
|
+
onEvent: () => () => {},
|
|
217
|
+
} as never,
|
|
218
|
+
keybindings: {
|
|
219
|
+
getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
|
|
220
|
+
onChange: () => () => {},
|
|
221
|
+
} as never,
|
|
222
|
+
refreshDiscovery: async () => [],
|
|
223
|
+
getDiscoveredProjects: () => [],
|
|
224
|
+
machineDisplayName: "Local Machine",
|
|
225
|
+
updateManager: null,
|
|
226
|
+
})
|
|
227
|
+
const wsA = new FakeWebSocket()
|
|
228
|
+
const wsB = new FakeWebSocket()
|
|
229
|
+
|
|
230
|
+
router.handleOpen(wsA as never)
|
|
231
|
+
router.handleOpen(wsB as never)
|
|
232
|
+
|
|
233
|
+
router.handleMessage(
|
|
234
|
+
wsA as never,
|
|
235
|
+
JSON.stringify({
|
|
236
|
+
v: 1,
|
|
237
|
+
type: "subscribe",
|
|
238
|
+
id: "sidebar-a",
|
|
239
|
+
topic: { type: "sidebar" },
|
|
240
|
+
})
|
|
241
|
+
)
|
|
242
|
+
router.handleMessage(
|
|
243
|
+
wsB as never,
|
|
244
|
+
JSON.stringify({
|
|
245
|
+
v: 1,
|
|
246
|
+
type: "subscribe",
|
|
247
|
+
id: "sidebar-b",
|
|
248
|
+
topic: { type: "sidebar" },
|
|
249
|
+
})
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
router.handleMessage(
|
|
253
|
+
wsA as never,
|
|
254
|
+
JSON.stringify({
|
|
255
|
+
v: 1,
|
|
256
|
+
type: "command",
|
|
257
|
+
id: "mark-read-1",
|
|
258
|
+
command: { type: "chat.markRead", chatId: "chat-1" },
|
|
259
|
+
})
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
await Promise.resolve()
|
|
263
|
+
|
|
264
|
+
expect(wsA.sent.at(-2)).toEqual({
|
|
265
|
+
v: PROTOCOL_VERSION,
|
|
266
|
+
type: "ack",
|
|
267
|
+
id: "mark-read-1",
|
|
268
|
+
})
|
|
269
|
+
expect(wsA.sent.at(-1)).toEqual({
|
|
270
|
+
v: PROTOCOL_VERSION,
|
|
271
|
+
type: "snapshot",
|
|
272
|
+
id: "sidebar-a",
|
|
273
|
+
snapshot: {
|
|
274
|
+
type: "sidebar",
|
|
275
|
+
data: {
|
|
276
|
+
projectGroups: [{
|
|
277
|
+
groupKey: "project-1",
|
|
278
|
+
localPath: "/tmp/project",
|
|
279
|
+
chats: [{
|
|
280
|
+
_id: "chat-1",
|
|
281
|
+
_creationTime: 1,
|
|
282
|
+
chatId: "chat-1",
|
|
283
|
+
title: "Chat",
|
|
284
|
+
status: "idle",
|
|
285
|
+
unread: false,
|
|
286
|
+
localPath: "/tmp/project",
|
|
287
|
+
provider: null,
|
|
288
|
+
lastMessageAt: undefined,
|
|
289
|
+
hasAutomation: false,
|
|
290
|
+
}],
|
|
291
|
+
}],
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
expect(wsB.sent.at(-1)).toEqual({
|
|
296
|
+
v: PROTOCOL_VERSION,
|
|
297
|
+
type: "snapshot",
|
|
298
|
+
id: "sidebar-b",
|
|
299
|
+
snapshot: {
|
|
300
|
+
type: "sidebar",
|
|
301
|
+
data: {
|
|
302
|
+
projectGroups: [{
|
|
303
|
+
groupKey: "project-1",
|
|
304
|
+
localPath: "/tmp/project",
|
|
305
|
+
chats: [{
|
|
306
|
+
_id: "chat-1",
|
|
307
|
+
_creationTime: 1,
|
|
308
|
+
chatId: "chat-1",
|
|
309
|
+
title: "Chat",
|
|
310
|
+
status: "idle",
|
|
311
|
+
unread: false,
|
|
312
|
+
localPath: "/tmp/project",
|
|
313
|
+
provider: null,
|
|
314
|
+
lastMessageAt: undefined,
|
|
315
|
+
hasAutomation: false,
|
|
316
|
+
}],
|
|
317
|
+
}],
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
test("broadcasts background title-generation errors to connected clients", () => {
|
|
324
|
+
let reportBackgroundError: ((message: string) => void) | null | undefined
|
|
325
|
+
const router = createWsRouter({
|
|
326
|
+
store: { state: createEmptyState() } as never,
|
|
327
|
+
agent: {
|
|
328
|
+
getActiveStatuses: () => new Map(),
|
|
329
|
+
setBackgroundErrorReporter: (reporter: ((message: string) => void) | null) => {
|
|
330
|
+
reportBackgroundError = reporter
|
|
331
|
+
},
|
|
332
|
+
} as never,
|
|
333
|
+
terminals: {
|
|
334
|
+
getSnapshot: () => null,
|
|
335
|
+
onEvent: () => () => {},
|
|
336
|
+
} as never,
|
|
337
|
+
keybindings: {
|
|
338
|
+
getSnapshot: () => DEFAULT_KEYBINDINGS_SNAPSHOT,
|
|
339
|
+
onChange: () => () => {},
|
|
340
|
+
} as never,
|
|
341
|
+
refreshDiscovery: async () => [],
|
|
342
|
+
getDiscoveredProjects: () => [],
|
|
343
|
+
machineDisplayName: "Local Machine",
|
|
344
|
+
updateManager: null,
|
|
345
|
+
})
|
|
346
|
+
const ws = new FakeWebSocket()
|
|
347
|
+
router.handleOpen(ws as never)
|
|
348
|
+
|
|
349
|
+
reportBackgroundError?.("[title-generation] chat chat-1 failed")
|
|
350
|
+
|
|
351
|
+
expect(ws.sent).toEqual([
|
|
352
|
+
{
|
|
353
|
+
v: PROTOCOL_VERSION,
|
|
354
|
+
type: "error",
|
|
355
|
+
message: "[title-generation] chat chat-1 failed",
|
|
356
|
+
},
|
|
357
|
+
])
|
|
358
|
+
})
|
|
359
|
+
|
|
179
360
|
test("subscribes to keybindings snapshots and writes keybindings through the router", async () => {
|
|
180
361
|
const initialSnapshot: KeybindingsSnapshot = DEFAULT_KEYBINDINGS_SNAPSHOT
|
|
181
362
|
const keybindings = {
|
package/src/server/ws-router.ts
CHANGED
|
@@ -138,6 +138,16 @@ export function createWsRouter({
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
function broadcastError(message: string) {
|
|
142
|
+
for (const ws of sockets) {
|
|
143
|
+
send(ws, {
|
|
144
|
+
v: PROTOCOL_VERSION,
|
|
145
|
+
type: "error",
|
|
146
|
+
message,
|
|
147
|
+
})
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
141
151
|
function pushTerminalSnapshot(terminalId: string) {
|
|
142
152
|
for (const ws of sockets) {
|
|
143
153
|
for (const [id, topic] of ws.data.subscriptions.entries()) {
|
|
@@ -183,6 +193,8 @@ export function createWsRouter({
|
|
|
183
193
|
}
|
|
184
194
|
}) ?? (() => {})
|
|
185
195
|
|
|
196
|
+
agent.setBackgroundErrorReporter?.(broadcastError)
|
|
197
|
+
|
|
186
198
|
async function handleCommand(ws: ServerWebSocket<ClientState>, message: Extract<ClientEnvelope, { type: "command" }>) {
|
|
187
199
|
const { command, id } = message
|
|
188
200
|
try {
|
|
@@ -275,6 +287,11 @@ export function createWsRouter({
|
|
|
275
287
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
276
288
|
break
|
|
277
289
|
}
|
|
290
|
+
case "chat.markRead": {
|
|
291
|
+
await store.setChatReadState(command.chatId, false)
|
|
292
|
+
send(ws, { v: PROTOCOL_VERSION, type: "ack", id })
|
|
293
|
+
break
|
|
294
|
+
}
|
|
278
295
|
case "chat.send": {
|
|
279
296
|
const result = await agent.send(command)
|
|
280
297
|
send(ws, { v: PROTOCOL_VERSION, type: "ack", id, result })
|
|
@@ -374,6 +391,7 @@ export function createWsRouter({
|
|
|
374
391
|
void handleCommand(ws, parsed)
|
|
375
392
|
},
|
|
376
393
|
dispose() {
|
|
394
|
+
agent.setBackgroundErrorReporter?.(null)
|
|
377
395
|
disposeTerminalEvents()
|
|
378
396
|
disposeKeybindingEvents()
|
|
379
397
|
disposeUpdateEvents()
|
package/src/shared/protocol.ts
CHANGED
|
@@ -62,6 +62,7 @@ export type ClientCommand =
|
|
|
62
62
|
| { type: "chat.create"; projectId: string }
|
|
63
63
|
| { type: "chat.rename"; chatId: string; title: string }
|
|
64
64
|
| { type: "chat.delete"; chatId: string }
|
|
65
|
+
| { type: "chat.markRead"; chatId: string }
|
|
65
66
|
| {
|
|
66
67
|
type: "chat.send"
|
|
67
68
|
chatId?: string
|