Skip to content

Commit 7700404

Browse files
Reduce session note render cost
Reuse derived transcript and tab state across the session note surface, avoid fallback NoteInput subscriptions when parent state is provided, and move pending upload processing out of TabContentNoteInner.
1 parent 3766866 commit 7700404

6 files changed

Lines changed: 183 additions & 52 deletions

File tree

apps/desktop/src/session/components/note-input/header.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1568,7 +1568,7 @@ export function useEditorTabs({
15681568
});
15691569
}
15701570

1571-
function createEditorTabs({
1571+
export function createEditorTabs({
15721572
enhancedNoteIds,
15731573
canShowInsights,
15741574
canShowTranscript,

apps/desktop/src/session/components/note-input/index.tsx

Lines changed: 90 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -38,58 +38,122 @@ export interface NoteInputHandle {
3838
prepareForTabChange: () => void;
3939
}
4040

41+
type NoteInputProps = {
42+
tab: Extract<Tab, { type: "sessions" }>;
43+
onNavigateToTitle?: (pixelWidth?: number) => void;
44+
onScroll?: UIEventHandler<HTMLDivElement>;
45+
editorTabs?: TabEditorView[];
46+
currentTab?: TabEditorView;
47+
handleTabChange?: (view: TabEditorView) => void;
48+
hideHeader?: boolean;
49+
sessionMode?: SessionMode;
50+
};
51+
4152
export function shouldShowTranscriptTabSpinner(sessionMode: SessionMode) {
4253
return sessionMode === "finalizing" || sessionMode === "running_batch";
4354
}
4455

45-
export const NoteInput = forwardRef<
56+
export const NoteInput = forwardRef<NoteInputHandle, NoteInputProps>(
57+
function NoteInput(props, ref) {
58+
if (
59+
props.editorTabs &&
60+
props.currentTab &&
61+
props.handleTabChange &&
62+
props.sessionMode !== undefined
63+
) {
64+
return (
65+
<NoteInputContent
66+
{...props}
67+
ref={ref}
68+
editorTabs={props.editorTabs}
69+
currentTab={props.currentTab}
70+
commitTabChange={props.handleTabChange}
71+
sessionMode={props.sessionMode}
72+
/>
73+
);
74+
}
75+
76+
return <NoteInputWithDerivedState {...props} ref={ref} />;
77+
},
78+
);
79+
80+
const NoteInputWithDerivedState = forwardRef<NoteInputHandle, NoteInputProps>(
81+
function NoteInputWithDerivedState(
82+
{ tab, editorTabs, currentTab, handleTabChange, ...props },
83+
ref,
84+
) {
85+
const fallbackEditorTabs = useEditorTabs({ sessionId: tab.id });
86+
const fallbackCurrentTab: TabEditorView = useCurrentNoteTab(tab);
87+
const updateSessionTabState = useTabs(
88+
(state) => state.updateSessionTabState,
89+
);
90+
const tabRef = useRef(tab);
91+
tabRef.current = tab;
92+
const sessionMode = useListener((state) => state.getSessionMode(tab.id));
93+
94+
const commitTabChange = useCallback(
95+
(tabView: TabEditorView) => {
96+
if (handleTabChange) {
97+
handleTabChange(tabView);
98+
return;
99+
}
100+
101+
updateSessionTabState(tabRef.current, {
102+
...tabRef.current.state,
103+
view: tabView,
104+
});
105+
},
106+
[handleTabChange, updateSessionTabState],
107+
);
108+
109+
return (
110+
<NoteInputContent
111+
{...props}
112+
ref={ref}
113+
tab={tab}
114+
editorTabs={editorTabs ?? fallbackEditorTabs}
115+
currentTab={currentTab ?? fallbackCurrentTab}
116+
commitTabChange={commitTabChange}
117+
sessionMode={props.sessionMode ?? sessionMode}
118+
/>
119+
);
120+
},
121+
);
122+
123+
const NoteInputContent = forwardRef<
46124
NoteInputHandle,
47-
{
48-
tab: Extract<Tab, { type: "sessions" }>;
49-
onNavigateToTitle?: (pixelWidth?: number) => void;
50-
onScroll?: UIEventHandler<HTMLDivElement>;
51-
editorTabs?: TabEditorView[];
52-
currentTab?: TabEditorView;
53-
handleTabChange?: (view: TabEditorView) => void;
54-
hideHeader?: boolean;
125+
Omit<NoteInputProps, "editorTabs" | "currentTab" | "handleTabChange"> & {
126+
editorTabs: TabEditorView[];
127+
currentTab: TabEditorView;
128+
commitTabChange: (view: TabEditorView) => void;
129+
sessionMode: SessionMode;
55130
}
56131
>(
57132
(
58133
{
59134
tab,
60135
onNavigateToTitle,
61136
onScroll,
62-
editorTabs: providedEditorTabs,
63-
currentTab: providedCurrentTab,
64-
handleTabChange: providedHandleTabChange,
137+
editorTabs,
138+
currentTab,
139+
commitTabChange,
65140
hideHeader = false,
141+
sessionMode,
66142
},
67143
ref,
68144
) => {
69-
const fallbackEditorTabs = useEditorTabs({ sessionId: tab.id });
70-
const updateSessionTabState = useTabs(
71-
(state) => state.updateSessionTabState,
72-
);
73145
const internalEditorRef = useRef<NoteEditorRef>(null);
74146
const [container, setContainer] = useState<HTMLDivElement | null>(null);
75147
const [view, setView] = useState<EditorView | null>(null);
76148

77149
const sessionId = tab.id;
78-
79-
const tabRef = useRef(tab);
80-
tabRef.current = tab;
81-
82-
const fallbackCurrentTab: TabEditorView = useCurrentNoteTab(tab);
83-
const editorTabs = providedEditorTabs ?? fallbackEditorTabs;
84-
const currentTab = providedCurrentTab ?? fallbackCurrentTab;
85150
const deferredCurrentTab = useDeferredValue(currentTab);
86151
const renderedCurrentTab = editorTabs.some((editorTab) =>
87152
isSameEditorView(editorTab, deferredCurrentTab),
88153
)
89154
? deferredCurrentTab
90155
: currentTab;
91156

92-
const sessionMode = useListener((state) => state.getSessionMode(sessionId));
93157
const isMeetingInProgress =
94158
sessionMode === "active" ||
95159
sessionMode === "finalizing" ||
@@ -127,22 +191,9 @@ export const NoteInput = forwardRef<
127191
}
128192

129193
onBeforeTabChange();
130-
if (providedHandleTabChange) {
131-
providedHandleTabChange(tabView);
132-
} else {
133-
updateSessionTabState(tabRef.current, {
134-
...tabRef.current.state,
135-
view: tabView,
136-
});
137-
}
194+
commitTabChange(tabView);
138195
},
139-
[
140-
currentTab,
141-
onBeforeTabChange,
142-
providedHandleTabChange,
143-
renderedCurrentTab,
144-
updateSessionTabState,
145-
],
196+
[commitTabChange, currentTab, onBeforeTabChange, renderedCurrentTab],
146197
);
147198

148199
const handleAdjacentViewShortcut = useCallback(

apps/desktop/src/session/components/shared.tsx

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { extractPlainText } from "~/search/contexts/engine/utils";
88
import { useCanShowInsights } from "~/session/insights/past-notes";
99
import { useMainStoreRowsRevision } from "~/store/tinybase/hooks";
1010
import * as main from "~/store/tinybase/store/main";
11+
import type { SessionMode } from "~/store/zustand/listener/general";
1112
import type { Tab } from "~/store/zustand/tabs/schema";
1213
import { type EditorView } from "~/store/zustand/tabs/schema";
1314
import { useListener } from "~/stt/contexts";
@@ -116,13 +117,36 @@ export function useCanShowTranscript(
116117
const hasTranscript = useHasTranscript(sessionId);
117118
const sessionMode = useListener((state) => state.getSessionMode(sessionId));
118119
const batchError = useListener((state) => state.batch[sessionId]?.error);
119-
const isLiveCapture =
120-
sessionMode === "active" || sessionMode === "finalizing";
121120
const hasLiveSegments = useListener(
122121
(state) =>
123122
state.live.sessionId === sessionId && state.liveSegments.length > 0,
124123
);
125124

125+
return getCanShowTranscript({
126+
audioExists,
127+
batchError: Boolean(batchError),
128+
hasLiveSegments,
129+
hasTranscript,
130+
sessionMode,
131+
});
132+
}
133+
134+
export function getCanShowTranscript({
135+
audioExists = false,
136+
batchError = false,
137+
hasLiveSegments = false,
138+
hasTranscript,
139+
sessionMode,
140+
}: {
141+
audioExists?: boolean;
142+
batchError?: boolean;
143+
hasLiveSegments?: boolean;
144+
hasTranscript: boolean;
145+
sessionMode: SessionMode;
146+
}): boolean {
147+
const isLiveCapture =
148+
sessionMode === "active" || sessionMode === "finalizing";
149+
126150
return (
127151
hasTranscript ||
128152
(audioExists && !isLiveCapture) ||

apps/desktop/src/session/hooks/useEnhancedNotes.test.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,10 @@ vi.mock("~/stt/contexts", () => ({
6565
}),
6666
}));
6767

68-
import { useEnsureDefaultSummary } from "./useEnhancedNotes";
68+
import {
69+
useEnsureDefaultSummary,
70+
useEnsureDefaultSummaryFromState,
71+
} from "./useEnhancedNotes";
6972

7073
describe("useEnsureDefaultSummary", () => {
7174
beforeEach(() => {
@@ -121,6 +124,23 @@ describe("useEnsureDefaultSummary", () => {
121124
});
122125
});
123126

127+
it("does not create the summary row before hydration enables it", async () => {
128+
renderHook(() =>
129+
useEnsureDefaultSummaryFromState({
130+
batchError: false,
131+
enabled: false,
132+
enhancedNoteCount: 0,
133+
hasTranscript: true,
134+
sessionId: "session-1",
135+
sessionMode: "inactive",
136+
}),
137+
);
138+
139+
await waitFor(() => {
140+
expect(hoisted.service.ensureNote).not.toHaveBeenCalled();
141+
});
142+
});
143+
124144
it("does not create the summary row while finalizing recording", async () => {
125145
hoisted.sessionMode = "finalizing";
126146

apps/desktop/src/session/hooks/useEnhancedNotes.ts

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useHasTranscript } from "~/session/components/shared";
66
import * as main from "~/store/tinybase/store/main";
77
import * as settings from "~/store/tinybase/store/settings";
88
import { createTaskId } from "~/store/zustand/ai-task/task-configs";
9+
import type { SessionMode } from "~/store/zustand/listener/general";
910
import { useListener } from "~/stt/contexts";
1011

1112
export function useEnhancedNotes(sessionId: string) {
@@ -54,35 +55,63 @@ export function useEnsureDefaultSummary(sessionId: string) {
5455
sessionId,
5556
main.STORE_ID,
5657
);
58+
useEnsureDefaultSummaryFromState({
59+
batchError: Boolean(batchError),
60+
enhancedNoteCount: enhancedNoteIds?.length ?? 0,
61+
hasTranscript,
62+
sessionId,
63+
sessionMode,
64+
});
65+
}
66+
67+
export function useEnsureDefaultSummaryFromState({
68+
batchError,
69+
enabled = true,
70+
enhancedNoteCount,
71+
hasTranscript,
72+
sessionId,
73+
sessionMode,
74+
}: {
75+
batchError: boolean;
76+
enabled?: boolean;
77+
enhancedNoteCount: number;
78+
hasTranscript: boolean;
79+
sessionId: string;
80+
sessionMode: SessionMode;
81+
}) {
5782
const selectedTemplateId = settings.UI.useValue(
5883
"selected_template_id",
5984
settings.STORE_ID,
6085
) as string | undefined;
86+
const templateId = selectedTemplateId || undefined;
6187

6288
useEffect(() => {
89+
if (!enabled) {
90+
return;
91+
}
92+
6393
const service = getEnhancerService();
6494
if (!service) {
6595
return;
6696
}
6797

68-
const hasEnhancedNotes = enhancedNoteIds && enhancedNoteIds.length > 0;
69-
const templateId = selectedTemplateId || undefined;
7098
const isLiveCapture =
7199
sessionMode === "active" || sessionMode === "finalizing";
72100
const canCreateSummary =
73101
!isLiveCapture &&
74-
(hasTranscript || sessionMode === "running_batch" || Boolean(batchError));
102+
(hasTranscript || sessionMode === "running_batch" || batchError);
75103

76-
if (!hasEnhancedNotes && canCreateSummary) {
104+
if (enhancedNoteCount === 0 && canCreateSummary) {
77105
service.ensureNote(sessionId, templateId);
78106
}
79107
}, [
80108
sessionId,
81-
enhancedNoteIds?.length,
82-
selectedTemplateId,
109+
enhancedNoteCount,
110+
templateId,
83111
hasTranscript,
84112
sessionMode,
85113
batchError,
114+
enabled,
86115
]);
87116
}
88117

apps/desktop/src/session/index.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,20 @@ import {
1212
type NoteInputHandle,
1313
} from "./components/note-input";
1414
import {
15+
createEditorTabs,
1516
Header as NoteInputHeader,
16-
useEditorTabs,
1717
} from "./components/note-input/header";
1818
import { SearchProvider } from "./components/note-input/search/context";
1919
import { OuterHeader } from "./components/outer-header";
2020
import { SessionSurface } from "./components/session-surface";
21-
import { useCurrentNoteTab } from "./components/shared";
21+
import {
22+
computeCurrentNoteTab,
23+
getCanShowTranscript,
24+
useHasTranscript,
25+
} from "./components/shared";
2226
import { useAutoEnhance } from "./hooks/useAutoEnhance";
27+
import { useEnsureDefaultSummaryFromState } from "./hooks/useEnhancedNotes";
28+
import { useCanShowInsights } from "./insights/past-notes";
2329
import { shouldShowSessionTopAudioPlayer } from "./top-audio-player";
2430

2531
import * as AudioPlayer from "~/audio-player";
@@ -155,14 +161,15 @@ function TabContentNoteInner({
155161
main.STORE_ID,
156162
) ?? [];
157163
const firstEnhancedNoteId = enhancedNoteIds[0];
164+
const contentHydrated = useHydrateSessionContent(sessionId);
158165
useEnsureDefaultSummaryFromState({
159166
batchError: Boolean(batchError),
167+
enabled: contentHydrated,
160168
enhancedNoteCount: enhancedNoteIds.length,
161169
hasTranscript,
162170
sessionId,
163171
sessionMode,
164172
});
165-
const contentHydrated = useHydrateSessionContent(sessionId);
166173
const updateSessionTabState = useTabs((state) => state.updateSessionTabState);
167174

168175
const { skipReason } = useAutoEnhance(tab);

0 commit comments

Comments
 (0)