mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
fix: ulid
This commit is contained in:
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user