Skip to content

Commit e082430

Browse files
committed
fix(frontend): null guards, side-effect-in-render, cleanup fixes
- tabbar: optional-chain ref.current in handleMouseUp find (crash if tab ref was null during drag end) and null guard in forEach reset loop - term: move NlGhostText construction from render body into useMemo to avoid side effect in render — prevents double-construction in strict mode - term: guard connectElemRef.current before ResizeObserver.observe() - nl-ghost-text: log warn on non-abort AI fetch errors instead of silent drop
1 parent 58f3e24 commit e082430

3 files changed

Lines changed: 13 additions & 9 deletions

File tree

frontend/app/tab/tabbar.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -487,8 +487,8 @@ const TabBar = memo(({ workspace, noTabs }: TabBarProps) => {
487487
const draggingTab = tabIds[tabIndex];
488488
const tabWidth = tabWidthRef.current;
489489
const finalLeftPosition = tabIndex * tabWidth;
490-
const ref = tabRefs.current.find((ref) => ref.current.dataset.tabId === draggingTab);
491-
if (ref.current) {
490+
const ref = tabRefs.current.find((ref) => ref.current?.dataset.tabId === draggingTab);
491+
if (ref?.current) {
492492
ref.current.classList.add("animate");
493493
ref.current.style.transform = `translate3d(${finalLeftPosition}px,0,0)`;
494494
}
@@ -498,8 +498,10 @@ const TabBar = memo(({ workspace, noTabs }: TabBarProps) => {
498498
} else {
499499
// Reset styles
500500
tabRefs.current.forEach((ref) => {
501-
ref.current.style.zIndex = "0";
502-
ref.current.classList.remove("animate");
501+
if (ref.current) {
502+
ref.current.style.zIndex = "0";
503+
ref.current.classList.remove("animate");
504+
}
503505
});
504506
// Reset dragging state
505507
setDraggingTab(null);

frontend/app/view/term/nl-ghost-text.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ export class NlGhostText {
196196
globalStore.set(this.stateAtom, { visible: true, command: cleaned, loading: false, query });
197197
} catch (e) {
198198
if (ctrl.signal.aborted) return;
199+
console.warn("[NlGhostText] AI fetch failed:", e);
199200
globalStore.set(this.stateAtom, { visible: false, command: "", loading: false, query });
200201
}
201202
}

frontend/app/view/term/term.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,9 @@ const TerminalView = ({ blockId, model }: ViewComponentProps<TermViewModel>) =>
194194
() => jotai.atom<GhostTextState>({ visible: false, command: "", loading: false, query: "" }),
195195
[]
196196
);
197-
const nlGhostRef = React.useRef<NlGhostText | null>(null);
198-
if (nlGhostRef.current == null) {
199-
nlGhostRef.current = new NlGhostText(ghostStateAtom);
200-
}
197+
// useMemo guarantees one instance per component lifetime (no side effect in render body)
198+
const nlGhostInst = React.useMemo(() => new NlGhostText(ghostStateAtom), []);
199+
const nlGhostRef = React.useRef<NlGhostText>(nlGhostInst);
201200
let termMode = blockData?.meta?.["term:mode"] ?? "term";
202201
if (termMode != "term" && termMode != "vdom") {
203202
termMode = "term";
@@ -363,7 +362,9 @@ const TerminalView = ({ blockId, model }: ViewComponentProps<TermViewModel>) =>
363362
const rszObs = new ResizeObserver(() => {
364363
termWrap.handleResize_debounced();
365364
});
366-
rszObs.observe(connectElemRef.current);
365+
if (connectElemRef.current) {
366+
rszObs.observe(connectElemRef.current);
367+
}
367368
termWrap.onSearchResultsDidChange = (results) => {
368369
globalStore.set(searchProps.resultsIndex, results.resultIndex);
369370
globalStore.set(searchProps.resultsCount, results.resultCount);

0 commit comments

Comments
 (0)