From 60e685d6e699f69420d02ec06a4218eac3c0c41e Mon Sep 17 00:00:00 2001 From: Melody Ruch <396921921@qq.com> Date: Sat, 15 Aug 2026 21:55:21 +0800 Subject: [PATCH] fix(redis): display console command before response --- .../redis/RedisKeyBrowser.expiry.spec.ts | 52 +++++++++++++++++++ .../src/components/redis/RedisKeyBrowser.vue | 29 ++++++----- 2 files changed, 67 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/components/redis/RedisKeyBrowser.expiry.spec.ts b/apps/desktop/src/components/redis/RedisKeyBrowser.expiry.spec.ts index 45673c50ea..746becd1e8 100644 --- a/apps/desktop/src/components/redis/RedisKeyBrowser.expiry.spec.ts +++ b/apps/desktop/src/components/redis/RedisKeyBrowser.expiry.spec.ts @@ -844,6 +844,58 @@ describe("RedisKeyBrowser command completion", () => { }); }); +describe("RedisKeyBrowser command console echo", () => { + it("echoes the submitted command to the terminal before the response arrives, then fills in the result", async () => { + const pending = deferred<{ value: unknown }>(); + mocks.redisExecuteCommand.mockReturnValueOnce(pending.promise); + mountBrowser(); + await settle(); + await openCommandPanel(); + await setCommandInput("PING"); + + const input = requiredElement("[data-redis-command-input]"); + input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); + await settle(); + + // The command must show up in the terminal right away — while the + // request is still in flight — not only once the response resolves. + const terminal = requiredElement(".redis-command-terminal"); + expect(terminal.textContent).toContain("PING"); + expect(terminal.querySelectorAll(".mb-2")).toHaveLength(1); + expect(terminal.textContent).not.toContain("PONG"); + + pending.resolve({ value: "PONG" }); + await settle(); + + expect(terminal.textContent).toContain("PONG"); + // The result fills in the same echoed entry rather than adding a second one. + expect(terminal.querySelectorAll(".mb-2")).toHaveLength(1); + }); + + it("attaches a failed command's error to the same echoed entry", async () => { + const pending = deferred<{ value: unknown }>(); + mocks.redisExecuteCommand.mockReturnValueOnce(pending.promise); + mountBrowser(); + await settle(); + await openCommandPanel(); + await setCommandInput("PING"); + + const input = requiredElement("[data-redis-command-input]"); + input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true })); + await settle(); + + const terminal = requiredElement(".redis-command-terminal"); + expect(terminal.textContent).toContain("PING"); + expect(terminal.querySelectorAll(".mb-2")).toHaveLength(1); + + pending.reject(new Error("ERR unknown command")); + await settle(); + + expect(terminal.textContent).toContain("ERR unknown command"); + expect(terminal.querySelectorAll(".mb-2")).toHaveLength(1); + }); +}); + describe("RedisKeyBrowser expiry creation", () => { it.each(["string", "hash", "list", "set", "zset", "stream", "json"] as const)("writes %s before applying one relative TTL", async (type) => { mountBrowser(); diff --git a/apps/desktop/src/components/redis/RedisKeyBrowser.vue b/apps/desktop/src/components/redis/RedisKeyBrowser.vue index 551a4eb9a3..a36ec3881e 100644 --- a/apps/desktop/src/components/redis/RedisKeyBrowser.vue +++ b/apps/desktop/src/components/redis/RedisKeyBrowser.vue @@ -936,8 +936,15 @@ function scrollCommandTerminalToEnd() { }); } -function appendCommandHistory(entry: Omit) { - commandHistory.value = [...commandHistory.value, { id: ++commandHistoryId, ...entry }]; +function appendCommandHistory(entry: Omit): number { + const id = ++commandHistoryId; + commandHistory.value = [...commandHistory.value, { id, ...entry }]; + scrollCommandTerminalToEnd(); + return id; +} + +function updateCommandHistory(id: number, patch: Partial>) { + commandHistory.value = commandHistory.value.map((entry) => (entry.id === id ? { ...entry, ...patch } : entry)); scrollCommandTerminalToEnd(); } @@ -955,14 +962,13 @@ function appendCommandOutput(entry: Omit) { async function runRedisCommand(command: string) { const prompt = commandPrompt.value; commandRunning.value = true; + // Echo the command to the terminal immediately so it doesn't look like the + // keystroke was lost while the request is in flight — the output is filled + // in on the same entry once the response (or error) arrives. + const entryId = appendCommandHistory({ prompt, command, output: "", error: false }); try { const result = await api.redisExecuteCommand(props.connectionId, commandDb.value, command, !props.blockDangerousRedisCommands); - appendCommandHistory({ - prompt, - command, - output: formatRedisConsoleValue(result.value), - error: false, - }); + updateCommandHistory(entryId, { output: formatRedisConsoleValue(result.value), error: false }); // The db this command ran on — capture before nextRedisCommandDb() advances it. const executedDb = commandDb.value; commandDb.value = nextRedisCommandDb(commandDb.value, command, result.value); @@ -980,12 +986,7 @@ async function runRedisCommand(command: string) { persistRedisHistory(command, true, result.value); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - appendCommandHistory({ - prompt, - command, - output: errorMessage, - error: true, - }); + updateCommandHistory(entryId, { output: errorMessage, error: true }); // Persist failed command too persistRedisHistory(command, false, null, errorMessage); } finally {