From 075cbb9d945157414928d58da2110bdde75f68c0 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 9 May 2026 23:02:31 -0400 Subject: [PATCH] refactor(id): localize prefixes --- .../prompt-input/build-request-parts.ts | 16 ++++----- .../app/src/components/prompt-input/submit.ts | 10 +++--- packages/app/src/pages/session.tsx | 7 ++-- packages/app/src/utils/id.ts | 34 +++++-------------- packages/opencode/src/control-plane/schema.ts | 5 +-- packages/opencode/src/id/id.ts | 24 ++++--------- packages/opencode/src/permission/schema.ts | 6 ++-- packages/opencode/src/pty/schema.ts | 5 +-- packages/opencode/src/question/schema.ts | 9 +++-- packages/opencode/src/session/schema.ts | 16 +++++---- packages/opencode/src/sync/schema.ts | 6 ++-- packages/opencode/src/tool/schema.ts | 5 +-- 12 files changed, 65 insertions(+), 78 deletions(-) diff --git a/packages/app/src/components/prompt-input/build-request-parts.ts b/packages/app/src/components/prompt-input/build-request-parts.ts index 98771aedd1..0cf1457caa 100644 --- a/packages/app/src/components/prompt-input/build-request-parts.ts +++ b/packages/app/src/components/prompt-input/build-request-parts.ts @@ -3,7 +3,7 @@ import { type AgentPartInput, type FilePartInput, type Part, type TextPartInput import type { FileSelection } from "@/context/file" import { encodeFilePath } from "@/context/file/path" import type { AgentPart, FileAttachmentPart, ImageAttachmentPart, Prompt } from "@/context/prompt" -import { Identifier } from "@/utils/id" +import { PartID } from "@/utils/id" import { createCommentMetadata, formatCommentNote } from "@/utils/comment-note" type PromptRequestPart = (TextPartInput | FilePartInput | AgentPartInput) & { id: string } @@ -91,7 +91,7 @@ const toOptimisticPart = (part: PromptRequestPart, sessionID: string, messageID: export function buildRequestParts(input: BuildRequestPartsInput) { const requestParts: PromptRequestPart[] = [ { - id: Identifier.ascending("part"), + id: PartID.ascending(), type: "text", text: input.text, }, @@ -100,7 +100,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) { const files = input.prompt.filter(isFileAttachment).map((attachment) => { const path = absolute(input.sessionDirectory, attachment.path) return { - id: Identifier.ascending("part"), + id: PartID.ascending(), type: "file", mime: "text/plain", url: `file://${encodeFilePath(path)}${fileQuery(attachment.selection)}`, @@ -119,7 +119,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) { const agents = input.prompt.filter(isAgentAttachment).map((attachment) => { return { - id: Identifier.ascending("part"), + id: PartID.ascending(), type: "agent", name: attachment.name, source: { @@ -139,7 +139,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) { used.add(url) const filePart = { - id: Identifier.ascending("part"), + id: PartID.ascending(), type: "file", mime: "text/plain", url, @@ -154,7 +154,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) { used.add(url) return [ { - id: Identifier.ascending("part"), + id: PartID.ascending(), type: "file", mime: "text/plain", url, @@ -165,7 +165,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) { return [ { - id: Identifier.ascending("part"), + id: PartID.ascending(), type: "text", text: formatCommentNote({ path: item.path, selection: item.selection, comment }), synthetic: true, @@ -184,7 +184,7 @@ export function buildRequestParts(input: BuildRequestPartsInput) { const images = input.images.map((attachment) => { return { - id: Identifier.ascending("part"), + id: PartID.ascending(), type: "file", mime: attachment.mime, url: attachment.dataUrl, diff --git a/packages/app/src/components/prompt-input/submit.ts b/packages/app/src/components/prompt-input/submit.ts index 05f0a3ed2c..e5d6fd28cd 100644 --- a/packages/app/src/components/prompt-input/submit.ts +++ b/packages/app/src/components/prompt-input/submit.ts @@ -13,7 +13,7 @@ import { usePermission } from "@/context/permission" import { type ContextItem, type ImageAttachmentPart, type Prompt, usePrompt } from "@/context/prompt" import { useSDK } from "@/context/sdk" import { useSync } from "@/context/sync" -import { Identifier } from "@/utils/id" +import { MessageID, PartID } from "@/utils/id" import { Worktree as WorktreeState } from "@/utils/worktree" import { buildRequestParts } from "./build-request-parts" import { setCursorPosition } from "./editor-dom" @@ -89,7 +89,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) { model: `${input.draft.model.providerID}/${input.draft.model.modelID}`, variant: input.draft.variant, parts: images.map((attachment) => ({ - id: Identifier.ascending("part"), + id: PartID.ascending(), type: "file" as const, mime: attachment.mime, url: attachment.dataUrl, @@ -103,7 +103,7 @@ export async function sendFollowupDraft(input: FollowupSendInput) { } } - const messageID = input.messageID ?? Identifier.ascending("message") + const messageID = input.messageID ?? MessageID.ascending() const { requestParts, optimisticParts } = buildRequestParts({ prompt: input.draft.prompt, context: input.draft.context, @@ -467,7 +467,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { model: `${model.providerID}/${model.modelID}`, variant, parts: images.map((attachment) => ({ - id: Identifier.ascending("part"), + id: PartID.ascending(), type: "file" as const, mime: attachment.mime, url: attachment.dataUrl, @@ -486,7 +486,7 @@ export function createPromptSubmit(input: PromptSubmitInput) { } const commentItems = context.filter((item) => item.type === "file" && !!item.comment?.trim()) - const messageID = Identifier.ascending("message") + const messageID = MessageID.ascending() const removeOptimisticMessage = () => { sync.session.optimistic.remove({ diff --git a/packages/app/src/pages/session.tsx b/packages/app/src/pages/session.tsx index 1345e355eb..e5aa9b0a3b 100644 --- a/packages/app/src/pages/session.tsx +++ b/packages/app/src/pages/session.tsx @@ -58,7 +58,7 @@ import { SessionSidePanel } from "@/pages/session/session-side-panel" import { TerminalPanel } from "@/pages/session/terminal-panel" import { useSessionCommands } from "@/pages/session/use-session-commands" import { useSessionHashScroll } from "@/pages/session/use-session-hash-scroll" -import { Identifier } from "@/utils/id" +import { MessageID } from "@/utils/id" import { diffs as list } from "@/utils/diffs" import { Persist, persisted } from "@/utils/persist" import { extractPromptFromParts } from "@/utils/prompt" @@ -1575,10 +1575,7 @@ export default function Page() { } const queueFollowup = (draft: FollowupDraft) => { - setFollowup("items", draft.sessionID, (items) => [ - ...(items ?? []), - { id: Identifier.ascending("message"), ...draft }, - ]) + setFollowup("items", draft.sessionID, (items) => [...(items ?? []), { id: MessageID.ascending(), ...draft }]) setFollowup("failed", draft.sessionID, undefined) setFollowup("paused", draft.sessionID, undefined) } diff --git a/packages/app/src/utils/id.ts b/packages/app/src/utils/id.ts index fa27cf4c5f..d67efc9347 100644 --- a/packages/app/src/utils/id.ts +++ b/packages/app/src/utils/id.ts @@ -1,31 +1,15 @@ -import z from "zod" - -const prefixes = { - session: "ses", - message: "msg", - permission: "per", - user: "usr", - part: "prt", - pty: "pty", -} as const - const LENGTH = 26 let lastTimestamp = 0 let counter = 0 -type Prefix = keyof typeof prefixes -export namespace Identifier { - export function schema(prefix: Prefix) { - return z.string().startsWith(prefixes[prefix]) - } +type Prefix = "msg" | "prt" - export function ascending(prefix: Prefix, given?: string) { - return generateID(prefix, false, given) - } +export const MessageID = { + ascending: (given?: string) => generateID("msg", false, given), +} - export function descending(prefix: Prefix, given?: string) { - return generateID(prefix, true, given) - } +export const PartID = { + ascending: (given?: string) => generateID("prt", false, given), } function generateID(prefix: Prefix, descending: boolean, given?: string): string { @@ -33,8 +17,8 @@ function generateID(prefix: Prefix, descending: boolean, given?: string): string return create(prefix, descending) } - if (!given.startsWith(prefixes[prefix])) { - throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`) + if (!given.startsWith(prefix)) { + throw new Error(`ID ${given} does not start with ${prefix}`) } return given @@ -61,7 +45,7 @@ function create(prefix: Prefix, descending: boolean, timestamp?: number): string timeBytes[i] = Number((now >> BigInt(40 - 8 * i)) & BigInt(0xff)) } - return prefixes[prefix] + "_" + bytesToHex(timeBytes) + randomBase62(LENGTH - 12) + return prefix + "_" + bytesToHex(timeBytes) + randomBase62(LENGTH - 12) } function bytesToHex(bytes: Uint8Array): string { diff --git a/packages/opencode/src/control-plane/schema.ts b/packages/opencode/src/control-plane/schema.ts index 53ce0cff97..b55b1aaec3 100644 --- a/packages/opencode/src/control-plane/schema.ts +++ b/packages/opencode/src/control-plane/schema.ts @@ -4,13 +4,14 @@ import { Identifier } from "@/id/id" import { zod } from "@opencode-ai/core/effect-zod" import { withStatics } from "@opencode-ai/core/schema" -const workspaceIdSchema = Schema.String.check(Schema.isStartsWith("wrk")).pipe(Schema.brand("WorkspaceID")) +const workspacePrefix = "wrk" +const workspaceIdSchema = Schema.String.check(Schema.isStartsWith(workspacePrefix)).pipe(Schema.brand("WorkspaceID")) export type WorkspaceID = typeof workspaceIdSchema.Type export const WorkspaceID = workspaceIdSchema.pipe( withStatics((schema: typeof workspaceIdSchema) => ({ - ascending: (id?: string) => schema.make(Identifier.ascending("workspace", id)), + ascending: (id?: string) => schema.make(Identifier.ascending(workspacePrefix, id)), zod: zod(schema), })), ) diff --git a/packages/opencode/src/id/id.ts b/packages/opencode/src/id/id.ts index 9e163cd6b8..4221a534dc 100644 --- a/packages/opencode/src/id/id.ts +++ b/packages/opencode/src/id/id.ts @@ -1,16 +1,6 @@ import { randomBytes } from "crypto" -const prefixes = { - event: "evt", - session: "ses", - message: "msg", - permission: "per", - question: "que", - part: "prt", - pty: "pty", - tool: "tool", - workspace: "wrk", -} as const +export type Prefix = "evt" | "ses" | "msg" | "per" | "que" | "prt" | "pty" | "tool" | "wrk" const LENGTH = 26 @@ -18,21 +8,21 @@ const LENGTH = 26 let lastTimestamp = 0 let counter = 0 -export function ascending(prefix: keyof typeof prefixes, given?: string) { +export function ascending(prefix: Prefix, given?: string) { return generateID(prefix, "ascending", given) } -export function descending(prefix: keyof typeof prefixes, given?: string) { +export function descending(prefix: Prefix, given?: string) { return generateID(prefix, "descending", given) } -function generateID(prefix: keyof typeof prefixes, direction: "descending" | "ascending", given?: string): string { +function generateID(prefix: Prefix, direction: "descending" | "ascending", given?: string): string { if (!given) { - return create(prefixes[prefix], direction) + return create(prefix, direction) } - if (!given.startsWith(prefixes[prefix])) { - throw new Error(`ID ${given} does not start with ${prefixes[prefix]}`) + if (!given.startsWith(prefix)) { + throw new Error(`ID ${given} does not start with ${prefix}`) } return given } diff --git a/packages/opencode/src/permission/schema.ts b/packages/opencode/src/permission/schema.ts index f7c6e2c5b7..2d13bb777a 100644 --- a/packages/opencode/src/permission/schema.ts +++ b/packages/opencode/src/permission/schema.ts @@ -4,12 +4,14 @@ import { Identifier } from "@/id/id" import { zod } from "@opencode-ai/core/effect-zod" import { Newtype } from "@opencode-ai/core/schema" +const permissionPrefix = "per" + export class PermissionID extends Newtype()( "PermissionID", - Schema.String.check(Schema.isStartsWith("per")), + Schema.String.check(Schema.isStartsWith(permissionPrefix)), ) { static ascending(id?: string): PermissionID { - return this.make(Identifier.ascending("permission", id)) + return this.make(Identifier.ascending(permissionPrefix, id)) } static readonly zod = zod(this) diff --git a/packages/opencode/src/pty/schema.ts b/packages/opencode/src/pty/schema.ts index fadb0457e7..a072f48f71 100644 --- a/packages/opencode/src/pty/schema.ts +++ b/packages/opencode/src/pty/schema.ts @@ -4,13 +4,14 @@ import { Identifier } from "@/id/id" import { zod } from "@opencode-ai/core/effect-zod" import { withStatics } from "@opencode-ai/core/schema" -const ptyIdSchema = Schema.String.check(Schema.isStartsWith("pty")).pipe(Schema.brand("PtyID")) +const ptyPrefix = "pty" +const ptyIdSchema = Schema.String.check(Schema.isStartsWith(ptyPrefix)).pipe(Schema.brand("PtyID")) export type PtyID = typeof ptyIdSchema.Type export const PtyID = ptyIdSchema.pipe( withStatics((schema: typeof ptyIdSchema) => ({ - ascending: (id?: string) => schema.make(Identifier.ascending("pty", id)), + ascending: (id?: string) => schema.make(Identifier.ascending(ptyPrefix, id)), zod: zod(schema), })), ) diff --git a/packages/opencode/src/question/schema.ts b/packages/opencode/src/question/schema.ts index 1856c94bc7..c9fc08839e 100644 --- a/packages/opencode/src/question/schema.ts +++ b/packages/opencode/src/question/schema.ts @@ -4,9 +4,14 @@ import { Identifier } from "@/id/id" import { zod } from "@opencode-ai/core/effect-zod" import { Newtype } from "@opencode-ai/core/schema" -export class QuestionID extends Newtype()("QuestionID", Schema.String.check(Schema.isStartsWith("que"))) { +const questionPrefix = "que" + +export class QuestionID extends Newtype()( + "QuestionID", + Schema.String.check(Schema.isStartsWith(questionPrefix)), +) { static ascending(id?: string): QuestionID { - return this.make(Identifier.ascending("question", id)) + return this.make(Identifier.ascending(questionPrefix, id)) } static readonly zod = zod(this) diff --git a/packages/opencode/src/session/schema.ts b/packages/opencode/src/session/schema.ts index 991c9ccc6b..e9f8faec4a 100644 --- a/packages/opencode/src/session/schema.ts +++ b/packages/opencode/src/session/schema.ts @@ -4,30 +4,34 @@ import { Identifier } from "@/id/id" import { zod } from "@opencode-ai/core/effect-zod" import { withStatics } from "@opencode-ai/core/schema" -export const SessionID = Schema.String.check(Schema.isStartsWith("ses")).pipe( +const sessionPrefix = "ses" +const messagePrefix = "msg" +const partPrefix = "prt" + +export const SessionID = Schema.String.check(Schema.isStartsWith(sessionPrefix)).pipe( Schema.brand("SessionID"), withStatics((s) => ({ - descending: (id?: string) => s.make(Identifier.descending("session", id)), + descending: (id?: string) => s.make(Identifier.descending(sessionPrefix, id)), zod: zod(s), })), ) export type SessionID = Schema.Schema.Type -export const MessageID = Schema.String.check(Schema.isStartsWith("msg")).pipe( +export const MessageID = Schema.String.check(Schema.isStartsWith(messagePrefix)).pipe( Schema.brand("MessageID"), withStatics((s) => ({ - ascending: (id?: string) => s.make(Identifier.ascending("message", id)), + ascending: (id?: string) => s.make(Identifier.ascending(messagePrefix, id)), zod: zod(s), })), ) export type MessageID = Schema.Schema.Type -export const PartID = Schema.String.check(Schema.isStartsWith("prt")).pipe( +export const PartID = Schema.String.check(Schema.isStartsWith(partPrefix)).pipe( Schema.brand("PartID"), withStatics((s) => ({ - ascending: (id?: string) => s.make(Identifier.ascending("part", id)), + ascending: (id?: string) => s.make(Identifier.ascending(partPrefix, id)), zod: zod(s), })), ) diff --git a/packages/opencode/src/sync/schema.ts b/packages/opencode/src/sync/schema.ts index dde2e53d17..c6eefaf7e4 100644 --- a/packages/opencode/src/sync/schema.ts +++ b/packages/opencode/src/sync/schema.ts @@ -4,10 +4,12 @@ import { Identifier } from "@/id/id" import { zod } from "@opencode-ai/core/effect-zod" import { withStatics } from "@opencode-ai/core/schema" -export const EventID = Schema.String.check(Schema.isStartsWith("evt")).pipe( +const eventPrefix = "evt" + +export const EventID = Schema.String.check(Schema.isStartsWith(eventPrefix)).pipe( Schema.brand("EventID"), withStatics((s) => ({ - ascending: (id?: string) => s.make(Identifier.ascending("event", id)), + ascending: (id?: string) => s.make(Identifier.ascending(eventPrefix, id)), zod: zod(s), })), ) diff --git a/packages/opencode/src/tool/schema.ts b/packages/opencode/src/tool/schema.ts index a80d915153..5a7510b5e0 100644 --- a/packages/opencode/src/tool/schema.ts +++ b/packages/opencode/src/tool/schema.ts @@ -4,13 +4,14 @@ import { Identifier } from "@/id/id" import { zod } from "@opencode-ai/core/effect-zod" import { withStatics } from "@opencode-ai/core/schema" -const toolIdSchema = Schema.String.check(Schema.isStartsWith("tool")).pipe(Schema.brand("ToolID")) +const toolPrefix = "tool" +const toolIdSchema = Schema.String.check(Schema.isStartsWith(toolPrefix)).pipe(Schema.brand("ToolID")) export type ToolID = typeof toolIdSchema.Type export const ToolID = toolIdSchema.pipe( withStatics((schema: typeof toolIdSchema) => ({ - ascending: (id?: string) => schema.make(Identifier.ascending("tool", id)), + ascending: (id?: string) => schema.make(Identifier.ascending(toolPrefix, id)), zod: zod(schema), })), )