kanna-code 0.17.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.
Files changed (36) hide show
  1. package/dist/client/assets/index-C952_xJD.css +32 -0
  2. package/dist/client/assets/index-lgfU0zw2.js +606 -0
  3. package/dist/client/chat-sounds/Blow.mp3 +0 -0
  4. package/dist/client/chat-sounds/Bottle.mp3 +0 -0
  5. package/dist/client/chat-sounds/Frog.mp3 +0 -0
  6. package/dist/client/chat-sounds/Funk.mp3 +0 -0
  7. package/dist/client/chat-sounds/Glass.mp3 +0 -0
  8. package/dist/client/chat-sounds/Ping.mp3 +0 -0
  9. package/dist/client/chat-sounds/Pop.mp3 +0 -0
  10. package/dist/client/chat-sounds/Purr.mp3 +0 -0
  11. package/dist/client/chat-sounds/Tink.mp3 +0 -0
  12. package/dist/client/index.html +2 -2
  13. package/package.json +2 -1
  14. package/src/server/agent.test.ts +153 -4
  15. package/src/server/agent.ts +76 -15
  16. package/src/server/event-store.test.ts +65 -0
  17. package/src/server/event-store.ts +27 -1
  18. package/src/server/events.ts +8 -0
  19. package/src/server/generate-title.ts +42 -2
  20. package/src/server/paths.ts +4 -0
  21. package/src/server/quick-response.test.ts +126 -3
  22. package/src/server/quick-response.ts +107 -14
  23. package/src/server/read-models.test.ts +4 -0
  24. package/src/server/read-models.ts +1 -0
  25. package/src/server/server.ts +178 -1
  26. package/src/server/title-generation.live.test.ts +44 -0
  27. package/src/server/uploads.test.ts +282 -0
  28. package/src/server/uploads.ts +127 -0
  29. package/src/server/ws-router.test.ts +181 -0
  30. package/src/server/ws-router.ts +18 -0
  31. package/src/shared/protocol.ts +3 -0
  32. package/src/shared/tools.test.ts +65 -0
  33. package/src/shared/tools.ts +76 -0
  34. package/src/shared/types.ts +34 -1
  35. package/dist/client/assets/index-CtKZIZdD.js +0 -533
  36. package/dist/client/assets/index-Cwex1U8S.css +0 -32
@@ -96,4 +96,69 @@ describe("hydrateToolResult", () => {
96
96
 
97
97
  expect(hydrateToolResult(tool, "line 1\nline 2")).toBe("line 1\nline 2")
98
98
  })
99
+
100
+ test("hydrates read image results with canonical image blocks intact", () => {
101
+ const tool = normalizeToolCall({
102
+ toolName: "Read",
103
+ toolId: "tool-image",
104
+ input: { file_path: "/tmp/example.png" },
105
+ })
106
+
107
+ expect(hydrateToolResult(tool, {
108
+ content: [
109
+ {
110
+ type: "text",
111
+ text: "Read image file [image/png]\n[Image: original 10x10, displayed at 10x10.]",
112
+ },
113
+ {
114
+ type: "image",
115
+ data: "ZmFrZS1pbWFnZS1kYXRh",
116
+ mimeType: "image/png",
117
+ },
118
+ ],
119
+ })).toEqual({
120
+ content: "Read image file [image/png]\n[Image: original 10x10, displayed at 10x10.]",
121
+ blocks: [
122
+ {
123
+ type: "text",
124
+ text: "Read image file [image/png]\n[Image: original 10x10, displayed at 10x10.]",
125
+ },
126
+ {
127
+ type: "image",
128
+ data: "ZmFrZS1pbWFnZS1kYXRh",
129
+ mimeType: "image/png",
130
+ },
131
+ ],
132
+ })
133
+ })
134
+
135
+ test("hydrates Claude read image results with source.base64 into canonical image blocks", () => {
136
+ const tool = normalizeToolCall({
137
+ toolName: "Read",
138
+ toolId: "tool-image-claude",
139
+ input: { file_path: "/tmp/example.png" },
140
+ })
141
+
142
+ expect(hydrateToolResult(tool, {
143
+ content: [
144
+ {
145
+ type: "image",
146
+ source: {
147
+ type: "base64",
148
+ data: "ZmFrZS1pbWFnZS1kYXRh",
149
+ media_type: "image/png",
150
+ },
151
+ },
152
+ ],
153
+ })).toEqual({
154
+ content: "",
155
+ blocks: [
156
+ {
157
+ type: "image",
158
+ data: "ZmFrZS1pbWFnZS1kYXRh",
159
+ mimeType: "image/png",
160
+ },
161
+ ],
162
+ })
163
+ })
99
164
  })
@@ -203,6 +203,72 @@ function parseJsonValue(value: unknown): unknown {
203
203
  }
204
204
  }
205
205
 
206
+ type ReadStructuredTextBlock = {
207
+ type: "text"
208
+ text: string
209
+ }
210
+
211
+ type ReadStructuredImageBlock = {
212
+ type: "image"
213
+ data: string
214
+ mimeType?: string
215
+ }
216
+
217
+ function normalizeReadBlocks(value: unknown): Array<ReadStructuredTextBlock | ReadStructuredImageBlock> {
218
+ const blocks = (
219
+ value
220
+ && typeof value === "object"
221
+ && "content" in value
222
+ && Array.isArray((value as { content?: unknown }).content)
223
+ )
224
+ ? (value as { content: unknown[] }).content
225
+ : Array.isArray(value)
226
+ ? value
227
+ : []
228
+
229
+ const normalized: Array<ReadStructuredTextBlock | ReadStructuredImageBlock> = []
230
+
231
+ for (const block of blocks) {
232
+ if (!block || typeof block !== "object" || !("type" in block)) {
233
+ continue
234
+ }
235
+
236
+ if (block.type === "text" && typeof block.text === "string") {
237
+ normalized.push({ type: "text", text: block.text })
238
+ continue
239
+ }
240
+
241
+ if (block.type === "image") {
242
+ if ("data" in block && typeof block.data === "string") {
243
+ normalized.push({
244
+ type: "image",
245
+ data: block.data,
246
+ mimeType: typeof block.mimeType === "string" ? block.mimeType : undefined,
247
+ })
248
+ continue
249
+ }
250
+
251
+ if (
252
+ "source" in block
253
+ && block.source
254
+ && typeof block.source === "object"
255
+ && "type" in block.source
256
+ && block.source.type === "base64"
257
+ && "data" in block.source
258
+ && typeof block.source.data === "string"
259
+ ) {
260
+ normalized.push({
261
+ type: "image",
262
+ data: block.source.data,
263
+ mimeType: typeof block.source.media_type === "string" ? block.source.media_type : undefined,
264
+ })
265
+ }
266
+ }
267
+ }
268
+
269
+ return normalized
270
+ }
271
+
206
272
  export function hydrateToolResult(tool: NormalizedToolCall, raw: unknown): HydratedToolCall["result"] {
207
273
  const parsed = parseJsonValue(raw)
208
274
 
@@ -241,6 +307,16 @@ export function hydrateToolResult(tool: NormalizedToolCall, raw: unknown): Hydra
241
307
  if (typeof parsed === "string") {
242
308
  return parsed
243
309
  }
310
+ const blocks = normalizeReadBlocks(parsed)
311
+ if (blocks.length > 0) {
312
+ return {
313
+ content: blocks
314
+ .flatMap((block) => block.type === "text" ? [block.text] : [])
315
+ .join(""),
316
+ blocks,
317
+ } satisfies ReadFileToolResult
318
+ }
319
+
244
320
  const record = asRecord(parsed)
245
321
  return {
246
322
  content: typeof record?.content === "string" ? record.content : JSON.stringify(parsed, null, 2),
@@ -3,6 +3,25 @@ export const PROTOCOL_VERSION = 1 as const
3
3
 
4
4
  export type AgentProvider = "claude" | "codex"
5
5
 
6
+ export type AttachmentKind = "image" | "file"
7
+
8
+ export interface ChatAttachment {
9
+ id: string
10
+ kind: AttachmentKind
11
+ displayName: string
12
+ absolutePath: string
13
+ relativePath: string
14
+ contentUrl: string
15
+ mimeType: string
16
+ size: number
17
+ }
18
+
19
+ export interface InternalUserAttachmentsData {
20
+ userText: string
21
+ attachments: ChatAttachment[]
22
+ llmHintText: string
23
+ }
24
+
6
25
  export interface ProviderModelOption {
7
26
  id: string
8
27
  label: string
@@ -173,6 +192,7 @@ export interface SidebarChatRow {
173
192
  chatId: string
174
193
  title: string
175
194
  status: KannaStatus
195
+ unread: boolean
176
196
  localPath: string
177
197
  provider: AgentProvider | null
178
198
  lastMessageAt?: number
@@ -378,6 +398,7 @@ export interface ToolResultEntry extends TranscriptEntryBase {
378
398
  export interface UserPromptEntry extends TranscriptEntryBase {
379
399
  kind: "user_prompt"
380
400
  content: string
401
+ attachments?: ChatAttachment[]
381
402
  }
382
403
 
383
404
  export interface SystemInitEntry extends TranscriptEntryBase {
@@ -501,8 +522,20 @@ export type HydratedBashToolCall =
501
522
  export type HydratedWebSearchToolCall =
502
523
  HydratedToolCallBase<"web_search", WebSearchToolCall["input"], unknown>
503
524
 
525
+ export interface ReadFileTextBlock {
526
+ type: "text"
527
+ text: string
528
+ }
529
+
530
+ export interface ReadFileImageBlock {
531
+ type: "image"
532
+ data: string
533
+ mimeType?: string
534
+ }
535
+
504
536
  export interface ReadFileToolResult {
505
537
  content: string
538
+ blocks?: Array<ReadFileTextBlock | ReadFileImageBlock>
506
539
  }
507
540
 
508
541
  export type HydratedReadFileToolCall =
@@ -540,7 +573,7 @@ export type HydratedToolCall =
540
573
  | HydratedUnknownToolCall
541
574
 
542
575
  export type HydratedTranscriptMessage =
543
- | ({ kind: "user_prompt"; content: string; id: string; messageId?: string; timestamp: string; hidden?: boolean })
576
+ | ({ kind: "user_prompt"; content: string; attachments?: ChatAttachment[]; id: string; messageId?: string; timestamp: string; hidden?: boolean })
544
577
  | ({ kind: "system_init"; model: string; tools: string[]; agents: string[]; slashCommands: string[]; mcpServers: McpServerInfo[]; provider: AgentProvider; id: string; messageId?: string; timestamp: string; hidden?: boolean; debugRaw?: string })
545
578
  | ({ kind: "account_info"; accountInfo: AccountInfo; id: string; messageId?: string; timestamp: string; hidden?: boolean })
546
579
  | ({ kind: "assistant_text"; text: string; id: string; messageId?: string; timestamp: string; hidden?: boolean })