Skip to content

Commit 6e155bc

Browse files
author
Suresh Sivasankaran
committed
Switch to zod for type guards and also approximate size for the metadata to be faster
1 parent 127a255 commit 6e155bc

File tree

2 files changed

+41
-63
lines changed

2 files changed

+41
-63
lines changed

cli/src/ui/messages/extension/__tests__/utils.test.ts

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -118,22 +118,22 @@ describe("formatByteSize", () => {
118118
expect(formatByteSize(1023)).toBe("1023 B")
119119
})
120120

121-
it("should format kilobytes", () => {
122-
expect(formatByteSize(1024)).toBe("1.0 KB")
123-
expect(formatByteSize(2048)).toBe("2.0 KB")
124-
expect(formatByteSize(1536)).toBe("1.5 KB")
121+
it("should format kilobytes with ~ prefix", () => {
122+
expect(formatByteSize(1024)).toBe("~1.0 KB")
123+
expect(formatByteSize(2048)).toBe("~2.0 KB")
124+
expect(formatByteSize(1536)).toBe("~1.5 KB")
125125
})
126126

127-
it("should format megabytes", () => {
128-
expect(formatByteSize(1024 * 1024)).toBe("1.0 MB")
129-
expect(formatByteSize(2.5 * 1024 * 1024)).toBe("2.5 MB")
130-
expect(formatByteSize(10 * 1024 * 1024)).toBe("10.0 MB")
127+
it("should format megabytes with ~ prefix", () => {
128+
expect(formatByteSize(1024 * 1024)).toBe("~1.0 MB")
129+
expect(formatByteSize(2.5 * 1024 * 1024)).toBe("~2.5 MB")
130+
expect(formatByteSize(10 * 1024 * 1024)).toBe("~10.0 MB")
131131
})
132132

133133
it("should round to one decimal place", () => {
134-
expect(formatByteSize(1536)).toBe("1.5 KB")
135-
expect(formatByteSize(1587)).toBe("1.5 KB") // Should round down
136-
expect(formatByteSize(1638)).toBe("1.6 KB") // Should round up
134+
expect(formatByteSize(1536)).toBe("~1.5 KB")
135+
expect(formatByteSize(1587)).toBe("~1.5 KB") // Should round down
136+
expect(formatByteSize(1638)).toBe("~1.6 KB") // Should round up
137137
})
138138
})
139139

@@ -225,7 +225,7 @@ describe("buildMetadataString", () => {
225225
}
226226

227227
const result = buildMetadataString(metadata)
228-
expect(result).toBe("JSON, 26 lines, 11.2 KB")
228+
expect(result).toBe("JSON, 26 lines, ~11.2 KB")
229229
})
230230
})
231231

cli/src/ui/messages/extension/utils.ts

Lines changed: 29 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { z } from "zod"
12
import type { ExtensionChatMessage } from "../../../types/messages.js"
23
import type { ToolData, McpServerData, FollowUpData, ApiReqInfo, ImageData } from "./types.js"
34

@@ -21,39 +22,32 @@ export function parseToolData(message: ExtensionChatMessage): ToolData | null {
2122
}
2223

2324
/**
24-
* Type guard to check if an object is valid McpServerData
25+
* Zod schema for MCP server data validation
2526
*/
26-
export function isMcpServerData(obj: any): obj is McpServerData {
27-
if (!obj || typeof obj !== "object") return false
28-
29-
// Check required fields
30-
if (!obj.type || typeof obj.serverName !== "string") return false
31-
32-
// Validate type field
33-
if (obj.type !== "use_mcp_tool" && obj.type !== "access_mcp_resource") return false
34-
35-
// Type-specific validation
36-
if (obj.type === "use_mcp_tool") {
37-
// Tool use should have toolName
38-
if (obj.toolName !== undefined && typeof obj.toolName !== "string") return false
39-
} else if (obj.type === "access_mcp_resource") {
40-
// Resource access should have uri
41-
if (obj.uri !== undefined && typeof obj.uri !== "string") return false
42-
}
43-
44-
// Validate optional fields if present
45-
if (obj.arguments !== undefined && typeof obj.arguments !== "string") return false
27+
const McpServerDataSchema = z.object({
28+
type: z.enum(["use_mcp_tool", "access_mcp_resource"]),
29+
serverName: z.string(),
30+
toolName: z.string().optional(),
31+
arguments: z.string().optional(),
32+
uri: z.string().optional(),
33+
response: z.any().optional(),
34+
})
4635

47-
return true
36+
/**
37+
* Type guard to check if an object is valid McpServerData
38+
* Uses Zod for validation
39+
*/
40+
export function isMcpServerData(obj: unknown): obj is McpServerData {
41+
return McpServerDataSchema.safeParse(obj).success
4842
}
4943

5044
/**
51-
* Parse MCP server data from message with validation
45+
* Parse MCP server data from message with Zod validation
5246
*/
5347
export function parseMcpServerData(message: ExtensionChatMessage): McpServerData | null {
54-
const parsed = parseMessageJson<McpServerData>(message.text)
55-
if (!parsed || !isMcpServerData(parsed)) return null
56-
return parsed
48+
const parsed = parseMessageJson(message.text)
49+
const result = McpServerDataSchema.safeParse(parsed)
50+
return result.success ? (result.data as McpServerData) : null
5751
}
5852

5953
/**
@@ -253,31 +247,14 @@ export function formatJson(jsonString: string, indent: number = 2): string | nul
253247
}
254248

255249
/**
256-
* Calculate byte size of string without creating full byte array for large strings
257-
* Uses chunked approach for strings over 10KB to avoid memory spike
250+
* Approximate byte size of string for display purposes
251+
* Uses 3x multiplier as conservative estimate (handles ASCII, UTF-8, emoji)
258252
*
259253
* @param str - The string to measure
260-
* @returns The byte size in UTF-8 encoding
254+
* @returns Approximate byte size
261255
*/
262-
function calculateByteSize(str: string): number {
263-
const CHUNK_THRESHOLD = 10000
264-
const CHUNK_SIZE = 10000
265-
266-
// For small strings, use TextEncoder directly (faster)
267-
if (str.length < CHUNK_THRESHOLD) {
268-
return new TextEncoder().encode(str).length
269-
}
270-
271-
// For large strings, chunk it to avoid memory spike
272-
const encoder = new TextEncoder()
273-
let totalBytes = 0
274-
275-
for (let i = 0; i < str.length; i += CHUNK_SIZE) {
276-
const chunk = str.slice(i, Math.min(i + CHUNK_SIZE, str.length))
277-
totalBytes += encoder.encode(chunk).length
278-
}
279-
280-
return totalBytes
256+
function approximateByteSize(str: string): number {
257+
return str.length * 3
281258
}
282259

283260
/**
@@ -323,7 +300,7 @@ export function formatContentWithMetadata(
323300
const lines = content.split("\n")
324301
const lineCount = lines.length
325302
const charCount = content.length
326-
const byteSize = calculateByteSize(content)
303+
const byteSize = approximateByteSize(content)
327304

328305
// Determine if preview is needed
329306
const isPreview = lineCount > maxLines
@@ -348,11 +325,12 @@ export function formatContentWithMetadata(
348325

349326
/**
350327
* Format byte size for display
328+
* Adds ~ prefix for approximations (KB/MB)
351329
*/
352330
export function formatByteSize(bytes: number): string {
353331
if (bytes < 1024) return `${bytes} B`
354-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
355-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
332+
if (bytes < 1024 * 1024) return `~${(bytes / 1024).toFixed(1)} KB`
333+
return `~${(bytes / (1024 * 1024)).toFixed(1)} MB`
356334
}
357335

358336
/**

0 commit comments

Comments
 (0)