From 6be823df41b105ddee33677f0b72aa4f5ad1c868 Mon Sep 17 00:00:00 2001 From: xuzhaorui <119386408+xuzhaorui@users.noreply.github.com> Date: Sun, 23 Aug 2026 11:54:48 +0800 Subject: [PATCH] fix(web): tolerate external project script ids in keybinding commands The server accepts any non-empty trimmed string as a ProjectScript id, but the client's SCRIPT_RUN_COMMAND_PATTERN only allowed lowercase slugs of up to 24 characters. An id dispatched over the API (e.g. a UUID) made commandForProjectScript throw during render, crashing every thread view and the project settings panel with no way to repair from the UI. Relax the pattern's middle segment to TrimmedNonEmptyString, matching the server-side constraint. UI-generated ids are still slugs via nextProjectScriptId, so nothing changes for normal usage; existing keybinding rules keep decoding. Fixes #7851 Worked on by ox-alpha via the pi coding agent harness. --- apps/web/src/projectScripts.test.ts | 6 ++++++ packages/contracts/src/keybindings.test.ts | 8 +++++++- packages/contracts/src/keybindings.ts | 7 ++----- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/web/src/projectScripts.test.ts b/apps/web/src/projectScripts.test.ts index 1f7a6bfaa9ff..e01c52c1f273 100644 --- a/apps/web/src/projectScripts.test.ts +++ b/apps/web/src/projectScripts.test.ts @@ -58,6 +58,12 @@ describe("projectScripts helpers", () => { const command = commandForProjectScript("lint"); expect(command).toBe("script.lint.run"); expect(projectScriptIdFromCommand(command)).toBe("lint"); + + const externalId = "123e4567-e89b-42d3-a456-426614174000"; + const externalCommand = commandForProjectScript(externalId); + expect(externalCommand).toBe(`script.${externalId}.run`); + expect(projectScriptIdFromCommand(externalCommand)).toBe(externalId); + expect(projectScriptIdFromCommand("terminal.toggle")).toBeNull(); }); diff --git a/packages/contracts/src/keybindings.test.ts b/packages/contracts/src/keybindings.test.ts index 71d8624a8aea..9d1bff8fd4ef 100644 --- a/packages/contracts/src/keybindings.test.ts +++ b/packages/contracts/src/keybindings.test.ts @@ -115,7 +115,7 @@ it.effect("rejects invalid command values", () => const result = yield* Effect.exit( decode(KeybindingRule, { key: "mod+j", - command: "script.Test.run", + command: "script..run", }), ); assert.strictEqual(result._tag, "Failure"); @@ -129,6 +129,12 @@ it.effect("accepts dynamic script run commands", () => command: "script.setup.run", }); assert.strictEqual(parsed.command, "script.setup.run"); + + const parsedExternalId = yield* decode(KeybindingRule, { + key: "mod+r", + command: "script.123e4567-e89b-42d3-a456-426614174000.run", + }); + assert.strictEqual(parsedExternalId.command, "script.123e4567-e89b-42d3-a456-426614174000.run"); }), ); diff --git a/packages/contracts/src/keybindings.ts b/packages/contracts/src/keybindings.ts index 19276c41e7b6..c5ac9ff0c7d6 100644 --- a/packages/contracts/src/keybindings.ts +++ b/packages/contracts/src/keybindings.ts @@ -1,5 +1,5 @@ import * as Schema from "effect/Schema"; -import { ForwardCompatibleArray, TrimmedString } from "./baseSchemas.ts"; +import { ForwardCompatibleArray, TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts"; export const MAX_KEYBINDING_VALUE_LENGTH = 64; export const MAX_KEYBINDING_WHEN_LENGTH = 256; @@ -77,10 +77,7 @@ export const STATIC_KEYBINDING_COMMANDS = [ export const SCRIPT_RUN_COMMAND_PATTERN = Schema.TemplateLiteral([ Schema.Literal("script."), - Schema.NonEmptyString.check( - Schema.isMaxLength(MAX_SCRIPT_ID_LENGTH), - Schema.isPattern(/^[a-z0-9][a-z0-9-]*$/), - ), + TrimmedNonEmptyString, Schema.Literal(".run"), ]);