fix: ulid

This commit is contained in:
Catriel Müller
2026-05-06 14:01:53 -03:00
parent 98866740af
commit 4aedc1b47f
6 changed files with 80 additions and 18 deletions
@@ -19,6 +19,7 @@ import { watchFontSizeConfig } from "../kilo-provider/font-size"
import { TokenManager } from "./token-manager"
import { KiloChatApiError, KiloChatClient } from "./kilo-chat-client"
import { EventServiceClient, WebSocketAuthError } from "./event-service-client"
import { ulid } from "./ulid"
import type {
ActionDeliveryFailedEvent,
BotStatusEvent,
@@ -206,7 +207,7 @@ export class KiloClawProvider implements vscode.Disposable {
await this.loadMoreConversations()
return
case "kiloclaw.sendMessage":
await this.sendMessage(msg.conversationId, msg.content, msg.clientId, msg.inReplyToMessageId)
await this.sendMessage(msg.conversationId, msg.content, msg.inReplyToMessageId)
return
case "kiloclaw.editMessage":
await this.editMessage(msg.conversationId, msg.messageId, msg.content)
@@ -790,12 +791,14 @@ export class KiloClawProvider implements vscode.Disposable {
private async sendMessage(
conversationId: string,
content: ContentBlock[],
clientId: string,
inReplyToMessageId?: string,
): Promise<void> {
if (!this.chat) return
if (!this.currentUserId) return
// kilo-chat validates clientId as a ULID (Crockford Base32); generate
// it here so the webview doesn't need to know the format.
const clientId = ulid()
const pendingId = `pending-${clientId}`
const optimistic: Message = {
id: pendingId,
@@ -28,11 +28,28 @@ export class KiloChatApiError extends Error {
public readonly status: number,
public readonly body: unknown,
) {
super(`KiloChat request failed: ${status}`)
super(`KiloChat request failed: ${status}${formatBodyDetail(body)}`)
this.name = "KiloChatApiError"
}
}
function formatBodyDetail(body: unknown): string {
if (body === null || body === undefined) return ""
if (typeof body === "string") return ` - ${body}`
if (typeof body === "object") {
const err = (body as Record<string, unknown>).error
if (typeof err === "string") return ` - ${err}`
// Fall back to a compact JSON dump so validation errors (zod issues, etc.)
// show up in the extension's Output channel without a separate logging hop.
try {
return ` - ${JSON.stringify(body)}`
} catch {
return ""
}
}
return ""
}
type HttpOpts = {
method?: string
body?: unknown
@@ -240,7 +240,6 @@ export type KiloClawInMessage =
conversationId: string
content: ContentBlock[]
inReplyToMessageId?: string
clientId: string
}
| { type: "kiloclaw.editMessage"; conversationId: string; messageId: string; content: ContentBlock[] }
| { type: "kiloclaw.deleteMessage"; conversationId: string; messageId: string }
+41
View File
@@ -0,0 +1,41 @@
/**
* Minimal ULID generator for the extension host.
*
* Produces a 26-character Crockford Base32 identifier: 10 time chars
* followed by 16 random chars. The kilo-chat worker validates clientId
* as a ULID, so `generateClientId` must emit only Crockford-legal
* characters — `toString(36)` is NOT safe because base36 includes
* I, L, O, and U which are excluded from Crockford Base32.
*/
// Crockford Base32 — no I, L, O, U (reduces transcription ambiguity).
const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
const ENCODING_LEN = ENCODING.length
const TIME_LEN = 10
const RANDOM_LEN = 16
function encodeTime(ts: number): string {
let out = ""
let n = ts
for (let i = 0; i < TIME_LEN; i++) {
const mod = n % ENCODING_LEN
out = ENCODING[mod] + out
n = (n - mod) / ENCODING_LEN
}
return out
}
function encodeRandom(): string {
const bytes = new Uint8Array(RANDOM_LEN)
globalThis.crypto.getRandomValues(bytes)
let out = ""
for (let i = 0; i < RANDOM_LEN; i++) {
out += ENCODING[bytes[i]! % ENCODING_LEN]
}
return out
}
/** Generate a ULID at the current epoch. */
export function ulid(): string {
return encodeTime(Date.now()) + encodeRandom()
}
@@ -86,18 +86,6 @@ export type ClawCtx = {
const ClawContext = createContext<ClawCtx>()
function generateClientId(): string {
// Lightweight ULID-ish identifier. The server only uses this to correlate
// optimistic messages with their server-assigned id, so collisions only
// matter within a single send burst (never realistic). 26 alphanum chars.
const ts = Date.now().toString(36).padStart(10, "0")
let rand = ""
for (let i = 0; i < 16; i++) {
rand += Math.floor(Math.random() * 36).toString(36)
}
return (ts + rand).toUpperCase().slice(0, 26)
}
export function ClawProvider(props: { children: JSX.Element }) {
const [phase, setPhase] = createSignal<Phase>("loading")
const [locale, setLocale] = createSignal<string | undefined>(undefined)
@@ -264,7 +252,6 @@ export function ClawProvider(props: { children: JSX.Element }) {
conversationId,
content,
inReplyToMessageId,
clientId: generateClientId(),
}),
editMessage: (conversationId, messageId, content) =>
vscode.postMessage({ type: "kiloclaw.editMessage", conversationId, messageId, content }),
@@ -28,11 +28,26 @@ export class KiloChatApiError extends Error {
public readonly status: number,
public readonly body: unknown,
) {
super(`KiloChat request failed: ${status}`)
super(`KiloChat request failed: ${status}${formatBodyDetail(body)}`)
this.name = "KiloChatApiError"
}
}
function formatBodyDetail(body: unknown): string {
if (body === null || body === undefined) return ""
if (typeof body === "string") return ` - ${body}`
if (typeof body === "object") {
const err = (body as Record<string, unknown>).error
if (typeof err === "string") return ` - ${err}`
try {
return ` - ${JSON.stringify(body)}`
} catch {
return ""
}
}
return ""
}
type HttpOpts = {
method?: string
body?: unknown