Skip to content

Commit 825256a

Browse files
cowhenclaude
andcommitted
Fix block rename and preview follow terminal issues
- Fix block rename Enter key not saving by removing early cancelRef check - Add shellType normalization to handle case-insensitive values - Fix suppressBidir flag stuck state with timeout fallback - Extract shell escape functions to separate module - Add percent sign escaping for cmd.exe paths - Refactor menu cleanup to reduce duplication Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent fddd346 commit 825256a

5 files changed

Lines changed: 51 additions & 70 deletions

File tree

frontend/app/block/blockframe-header.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,6 @@ const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: Head
104104

105105
const saveRename = React.useCallback(
106106
async (newTitle: string, sessionId: number) => {
107-
if (cancelRef.current) {
108-
cancelRef.current = false;
109-
return;
110-
}
111107
const val = newTitle.trim() || null;
112108
try {
113109
await waveEnv.rpc.SetMetaCommand(TabRpcClient, {
@@ -127,6 +123,7 @@ const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: Head
127123
React.useEffect(() => {
128124
if (isRenaming) {
129125
sessionIdRef.current++;
126+
cancelRef.current = false;
130127
}
131128
}, [isRenaming]);
132129

frontend/app/view/preview/preview-model.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -632,22 +632,23 @@ export class PreviewModel implements ViewModel {
632632
this.updateOpenFileModalAndError(!modalOpen);
633633
}
634634

635-
async goHistory(newPath: string) {
635+
async goHistory(newPath: string): Promise<boolean> {
636636
let fileName = globalStore.get(this.metaFilePath);
637637
if (fileName == null) {
638638
fileName = "";
639639
}
640640
const blockMeta = globalStore.get(this.blockAtom)?.meta;
641641
const updateMeta = goHistory("file", fileName, newPath, blockMeta);
642642
if (updateMeta == null) {
643-
return;
643+
return false;
644644
}
645645
const blockOref = WOS.makeORef("block", this.blockId);
646646
await this.env.services.object.UpdateObjectMeta(blockOref, updateMeta);
647647

648648
// Clear the saved file buffers
649649
globalStore.set(this.fileContentSaved, null);
650650
globalStore.set(this.newFileContent, null);
651+
return true;
651652
}
652653

653654
async goParentDirectory({ fileInfo = null }: { fileInfo?: FileInfo | null }) {

frontend/app/view/preview/preview.test.ts

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import { describe, expect, it } from "vitest";
5-
6-
// Note: These functions are tested by importing the module and accessing them
7-
// In a real setup, you'd export these from a shared utility module
8-
// For now, we'll duplicate the logic for testing purposes
9-
10-
function posixEscapePath(path: string): string {
11-
if (path === "~") return "~";
12-
if (path.startsWith("~/")) {
13-
return "~/" + "'" + path.slice(2).replace(/'/g, "'\\''") + "'";
14-
}
15-
return "'" + path.replace(/'/g, "'\\''") + "'";
16-
}
17-
18-
function pwshEscapePath(path: string): string {
19-
return "'" + path.replace(/'/g, "''") + "'";
20-
}
21-
22-
function cmdEscapePath(path: string): string {
23-
return '"' + path.replace(/"/g, '""') + '"';
24-
}
25-
26-
function buildCdCommand(shellType: string, path: string): string {
27-
if (shellType === "pwsh" || shellType === "powershell") {
28-
return "\x1bSet-Location -LiteralPath " + pwshEscapePath(path) + "\r";
29-
}
30-
if (shellType === "cmd") {
31-
return "\x1bcd /d " + cmdEscapePath(path) + "\r";
32-
}
33-
return "\x15cd " + posixEscapePath(path) + "\r";
34-
}
5+
import { buildCdCommand, cmdEscapePath, posixEscapePath, pwshEscapePath } from "./shellescape";
356

367
describe("posixEscapePath", () => {
378
it("handles tilde-only path", () => {
@@ -110,6 +81,10 @@ describe("cmdEscapePath", () => {
11081
expect(cmdEscapePath("C:\\Program Files")).toBe('"C:\\Program Files"');
11182
});
11283

84+
it("handles path with percent signs in cmd.exe", () => {
85+
expect(cmdEscapePath("C:\\100%complete")).toBe('"C:\\100%%complete"');
86+
});
87+
11388
it("handles empty string", () => {
11489
expect(cmdEscapePath("")).toBe('""');
11590
});

frontend/app/view/preview/preview.tsx

Lines changed: 13 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -21,32 +21,7 @@ import { MarkdownPreview } from "./preview-markdown";
2121
import type { PreviewModel } from "./preview-model";
2222
import { StreamingPreview } from "./preview-streaming";
2323
import type { PreviewEnv } from "./previewenv";
24-
25-
function posixEscapePath(path: string): string {
26-
if (path === "~") return "~";
27-
if (path.startsWith("~/")) {
28-
return "~/" + "'" + path.slice(2).replace(/'/g, "'\\''") + "'";
29-
}
30-
return "'" + path.replace(/'/g, "'\\''") + "'";
31-
}
32-
33-
function pwshEscapePath(path: string): string {
34-
return "'" + path.replace(/'/g, "''") + "'";
35-
}
36-
37-
function cmdEscapePath(path: string): string {
38-
return '"' + path.replace(/"/g, '""') + '"';
39-
}
40-
41-
function buildCdCommand(shellType: string, path: string): string {
42-
if (shellType === "pwsh" || shellType === "powershell") {
43-
return "\x1bSet-Location -LiteralPath " + pwshEscapePath(path) + "\r";
44-
}
45-
if (shellType === "cmd") {
46-
return "\x1bcd /d " + cmdEscapePath(path) + "\r";
47-
}
48-
return "\x15cd " + posixEscapePath(path) + "\r";
49-
}
24+
import { buildCdCommand } from "./shellescape";
5025

5126
async function sendCdToTerminal(termBlockId: string, path: string) {
5227
const block = WOS.getObjectValue<Block>(WOS.makeORef("block", termBlockId), globalStore.get);
@@ -188,16 +163,14 @@ function FollowTermDropdown({ model }: { model: PreviewModel }) {
188163

189164
const { pos, terms, currentFollowId, bidir } = menuData;
190165
const linkTerm = (blockId: string) => {
191-
BlockModel.getInstance().setBlockHighlight(null);
192166
fireAndForget(async () => {
193-
const updates: Record<string, any> = { "preview:followtermid": blockId };
167+
const updates: Record<string, string | boolean> = { "preview:followtermid": blockId };
194168
if (blockId !== currentFollowId) {
195169
updates["preview:followterm:bidir"] = false;
196170
}
197171
await model.env.services.object.UpdateObjectMeta(WOS.makeORef("block", model.blockId), updates);
172+
closeMenu();
198173
});
199-
globalStore.set(model.followTermMenuDataAtom, null);
200-
restoreFocus();
201174
};
202175
const toggleBidir = () => {
203176
fireAndForget(async () => {
@@ -213,9 +186,8 @@ function FollowTermDropdown({ model }: { model: PreviewModel }) {
213186
"preview:followtermid": null,
214187
"preview:followterm:bidir": null,
215188
});
189+
closeMenu();
216190
});
217-
globalStore.set(model.followTermMenuDataAtom, null);
218-
restoreFocus();
219191
};
220192

221193
const dropdownStyle: React.CSSProperties = {
@@ -345,9 +317,16 @@ function PreviewView({
345317
if (!followTermId || !followTermCwd) return;
346318
const currentPath = globalStore.get(model.metaFilePath) ?? "";
347319
if (followTermCwd !== currentPath) {
348-
suppressBidirRef.current = true;
320+
fireAndForget(async () => {
321+
const updated = await model.goHistory(followTermCwd);
322+
if (updated) {
323+
suppressBidirRef.current = true;
324+
setTimeout(() => {
325+
suppressBidirRef.current = false;
326+
}, 400);
327+
}
328+
});
349329
}
350-
fireAndForget(() => model.goHistory(followTermCwd));
351330
}, [followTermCwd, followTermId, model]);
352331

353332
useEffect(() => {
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
export function posixEscapePath(path: string): string {
5+
if (path === "~") return "~";
6+
if (path.startsWith("~/")) {
7+
return "~/" + "'" + path.slice(2).replace(/'/g, "'\\''") + "'";
8+
}
9+
return "'" + path.replace(/'/g, "'\\''") + "'";
10+
}
11+
12+
export function pwshEscapePath(path: string): string {
13+
return "'" + path.replace(/'/g, "''") + "'";
14+
}
15+
16+
export function cmdEscapePath(path: string): string {
17+
return '"' + path.replace(/%/g, "%%").replace(/"/g, '""') + '"';
18+
}
19+
20+
export function buildCdCommand(shellType: string, path: string): string {
21+
const normalizedShellType = (shellType || "").toLowerCase();
22+
if (normalizedShellType === "pwsh" || normalizedShellType === "powershell") {
23+
return "\x1bSet-Location -LiteralPath " + pwshEscapePath(path) + "\r";
24+
}
25+
if (normalizedShellType === "cmd") {
26+
return "\x1bcd /d " + cmdEscapePath(path) + "\r";
27+
}
28+
return "\x15cd " + posixEscapePath(path) + "\r";
29+
}

0 commit comments

Comments
 (0)