Skip to content

Commit a28a45e

Browse files
cowhenclaude
andcommitted
Improve block rename and preview follow terminal features
Block rename improvements: - Restrict rename menu to terminal blocks only (match badge visibility) - Disable rename for preview/ephemeral blocks - Add race condition protection for Escape/blur handling - Auto-select text on focus for better UX - Await RPC calls and log errors instead of fire-and-forget Preview follow terminal enhancements: - Add keyboard accessibility (Escape, Enter, Space navigation) - Add ARIA roles and labels for screen readers - Implement focus management (capture and restore) - Fix effect dependencies to prevent stale closures - Improve bidir suppression with timeout cleanup - Make sendCdToTerminal shell-aware (POSIX, PowerShell, cmd.exe) - Clear bidir state when linking new terminal - Fix terminal numbering to use tab order instead of filtered index - Move helper functions after imports for better organization Dev build improvements: - Update CLAUDE.md with explicit config/data env variables - Make launch_wave_dev.command portable with dynamic paths - Document why explicit overrides needed for clean installs Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 0a2e3a9 commit a28a45e

5 files changed

Lines changed: 191 additions & 45 deletions

File tree

CLAUDE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ This project uses a set of "skill" guides — focused how-to documents for commo
2525
- **Build**: `task package` (requires `PATH="/opt/homebrew/bin:$PATH"` for Go/Task). Builds as `Wave Dev.app` in `make/mac-arm64/`.
2626
- **Launch**: Use `launch_wave_dev.command` or run directly:
2727
```bash
28-
WAVETERM_HOME=~/.waveterm-dev make/mac-arm64/Wave\ Dev.app/Contents/MacOS/Wave\ Dev
28+
WAVETERM_HOME=~/.waveterm-dev \
29+
WAVETERM_CONFIG_HOME=~/.waveterm-dev/config \
30+
WAVETERM_DATA_HOME=~/.waveterm-dev/data \
31+
make/mac-arm64/Wave\ Dev.app/Contents/MacOS/Wave\ Dev
2932
```
30-
`WAVETERM_HOME` gives the dev build a separate data directory (`~/.waveterm-dev`) from the vanilla install (`~/.waveterm`).
33+
These variables create isolated config and data directories for the dev build. Note: `getWaveHomeDir()` only honors `WAVETERM_HOME` after `wave.lock` exists, so explicit `CONFIG/DATA` overrides are needed for clean installs and newly launched dev instances.
3134
- **Do not modify Info.plist or re-sign** the built app bundle — it breaks code signing on macOS and causes crashes.

frontend/app/block/blockframe-header.tsx

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,24 @@ function handleHeaderContextMenu(
3737
blockId: string,
3838
viewModel: ViewModel,
3939
nodeModel: NodeModel,
40-
blockEnv: BlockEnv
40+
blockEnv: BlockEnv,
41+
preview: boolean
4142
) {
4243
e.preventDefault();
4344
e.stopPropagation();
4445
const magnified = globalStore.get(nodeModel.isMagnified);
45-
const menu: ContextMenuItem[] = [
46-
{
46+
const ephemeral = globalStore.get(nodeModel.isEphemeral);
47+
const useTermHeader = viewModel?.useTermHeader ? globalStore.get(viewModel.useTermHeader) : false;
48+
const menu: ContextMenuItem[] = [];
49+
50+
if (!ephemeral && !preview && useTermHeader) {
51+
menu.push({
4752
label: "Rename Block",
4853
click: () => startBlockRename(blockId),
49-
},
54+
});
55+
}
56+
57+
menu.push(
5058
{
5159
label: magnified ? "Un-Magnify Block" : "Magnify Block",
5260
click: () => {
@@ -59,8 +67,8 @@ function handleHeaderContextMenu(
5967
click: () => {
6068
navigator.clipboard.writeText(blockId);
6169
},
62-
},
63-
];
70+
}
71+
);
6472
const extraItems = viewModel?.getSettingsMenuItems?.();
6573
if (extraItems && extraItems.length > 0) menu.push({ type: "separator" }, ...extraItems);
6674
menu.push(
@@ -91,14 +99,23 @@ const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: Head
9199
const useTermHeader = util.useAtomValueSafe(viewModel?.useTermHeader);
92100
let headerTextUnion = util.useAtomValueSafe(viewModel?.viewText);
93101
headerTextUnion = frameText ?? headerTextUnion;
102+
const cancelRef = React.useRef(false);
94103

95104
const saveRename = React.useCallback(
96-
(newTitle: string) => {
105+
async (newTitle: string) => {
106+
if (cancelRef.current) {
107+
cancelRef.current = false;
108+
return;
109+
}
97110
const val = newTitle.trim() || null;
98-
waveEnv.rpc.SetMetaCommand(TabRpcClient, {
99-
oref: WOS.makeORef("block", blockId),
100-
meta: { "frame:title": val },
101-
});
111+
try {
112+
await waveEnv.rpc.SetMetaCommand(TabRpcClient, {
113+
oref: WOS.makeORef("block", blockId),
114+
meta: { "frame:title": val },
115+
});
116+
} catch (error) {
117+
console.error("Failed to save block rename:", error);
118+
}
102119
stopBlockRename();
103120
},
104121
[blockId, waveEnv]
@@ -112,11 +129,20 @@ const HeaderTextElems = React.memo(({ viewModel, blockId, preview, error }: Head
112129
defaultValue={frameTitle ?? ""}
113130
placeholder="Block name..."
114131
className="block-frame-rename-input bg-transparent border border-white/20 rounded px-2 py-0.5 text-sm outline-none focus:border-white/40 min-w-0 w-full max-w-[200px]"
115-
onBlur={(e) => saveRename(e.currentTarget.value)}
132+
onFocus={(e) => e.currentTarget.select()}
133+
onBlur={(e) => {
134+
if (cancelRef.current) {
135+
cancelRef.current = false;
136+
stopBlockRename();
137+
return;
138+
}
139+
saveRename(e.currentTarget.value);
140+
}}
116141
onKeyDown={(e) => {
117142
if (e.key === "Enter") {
118143
saveRename(e.currentTarget.value);
119144
} else if (e.key === "Escape") {
145+
cancelRef.current = true;
120146
stopBlockRename();
121147
}
122148
}}
@@ -174,9 +200,10 @@ type HeaderEndIconsProps = {
174200
viewModel: ViewModel;
175201
nodeModel: NodeModel;
176202
blockId: string;
203+
preview: boolean;
177204
};
178205

179-
const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndIconsProps) => {
206+
const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId, preview }: HeaderEndIconsProps) => {
180207
const blockEnv = useWaveEnv<BlockEnv>();
181208
const endIconButtons = util.useAtomValueSafe(viewModel?.endIconButtons);
182209
const magnified = jotai.useAtomValue(nodeModel.isMagnified);
@@ -226,7 +253,7 @@ const HeaderEndIcons = React.memo(({ viewModel, nodeModel, blockId }: HeaderEndI
226253
elemtype: "iconbutton",
227254
icon: "cog",
228255
title: "Settings",
229-
click: (e) => handleHeaderContextMenu(e, blockId, viewModel, nodeModel, blockEnv),
256+
click: (e) => handleHeaderContextMenu(e, blockId, viewModel, nodeModel, blockEnv, preview),
230257
};
231258
endIconsElem.push(<IconButton key="settings" decl={settingsDecl} className="block-frame-settings" />);
232259
if (ephemeral) {
@@ -309,7 +336,7 @@ const BlockFrame_Header = ({
309336
className={cn("block-frame-default-header", useTermHeader && "!pl-[2px]")}
310337
data-role="block-header"
311338
ref={dragHandleRef}
312-
onContextMenu={(e) => handleHeaderContextMenu(e, nodeModel.blockId, viewModel, nodeModel, waveEnv)}
339+
onContextMenu={(e) => handleHeaderContextMenu(e, nodeModel.blockId, viewModel, nodeModel, waveEnv, preview)}
313340
>
314341
{!useTermHeader && (
315342
<>
@@ -344,7 +371,7 @@ const BlockFrame_Header = ({
344371
</div>
345372
)}
346373
<HeaderTextElems viewModel={viewModel} blockId={nodeModel.blockId} preview={preview} error={error} />
347-
<HeaderEndIcons viewModel={viewModel} nodeModel={nodeModel} blockId={nodeModel.blockId} />
374+
<HeaderEndIcons viewModel={viewModel} nodeModel={nodeModel} blockId={nodeModel.blockId} preview={preview} />
348375
</div>
349376
);
350377
};

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

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -520,17 +520,22 @@ export class PreviewModel implements ViewModel {
520520
showFollowTermMenu(e: React.MouseEvent<any>) {
521521
const tabData = globalStore.get(this.tabModel.tabAtom);
522522
const blockIds = tabData?.blockids ?? [];
523-
const terms = blockIds
524-
.filter((bid) => bid !== this.blockId)
525-
.map((bid) => {
523+
524+
const termBlockIds = blockIds
525+
.filter((bid) => {
526+
if (bid === this.blockId) return false;
526527
const block = WOS.getObjectValue<Block>(WOS.makeORef("block", bid), globalStore.get);
527-
return { blockId: bid, block };
528-
})
529-
.filter(({ block }) => block?.meta?.view === "term")
530-
.map(({ blockId: bid, block }, i) => ({
528+
return block?.meta?.view === "term";
529+
});
530+
531+
const terms = termBlockIds.map((bid) => {
532+
const block = WOS.getObjectValue<Block>(WOS.makeORef("block", bid), globalStore.get);
533+
const termIndex = termBlockIds.indexOf(bid) + 1;
534+
return {
531535
blockId: bid,
532-
title: (block?.meta?.["frame:title"] as string) || `Terminal ${i + 1}`,
533-
}));
536+
title: (block?.meta?.["frame:title"] as string) || `Terminal ${termIndex}`,
537+
};
538+
});
534539

535540
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
536541
const currentFollowId = globalStore.get(this.followTermIdAtom);

0 commit comments

Comments
 (0)