Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,20 +294,16 @@ export interface SidebarHoverPrewarmController {
export function createSidebarHoverPrewarmController(input: {
delayMs: number;
onPrewarmTargetChange: (threadKey: string | null) => void;
setTimeoutFn?: typeof globalThis.setTimeout;
clearTimeoutFn?: typeof globalThis.clearTimeout;
}): SidebarHoverPrewarmController {
const setTimeoutFn = input.setTimeoutFn ?? globalThis.setTimeout;
const clearTimeoutFn = input.clearTimeoutFn ?? globalThis.clearTimeout;
let target: string | null = null;
let pendingKey: string | null = null;
let timeoutId: NodeJS.Timeout | null = null;
let timeoutId: ReturnType<typeof setTimeout> | null = null;

const clearPending = () => {
if (timeoutId === null) {
return;
}
clearTimeoutFn(timeoutId);
clearTimeout(timeoutId);
timeoutId = null;
pendingKey = null;
};
Expand All @@ -334,7 +330,7 @@ export function createSidebarHoverPrewarmController(input: {

clearPending();
pendingKey = threadKey;
timeoutId = setTimeoutFn(() => {
timeoutId = setTimeout(() => {
timeoutId = null;
pendingKey = null;
target = threadKey;
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,8 @@ function SidebarThreadDetailPrewarmer({ threadRef }: { readonly threadRef: Scope
}

// Self-contained so hover changes re-render only this component, never the
// sidebar tree. Delegated `pointerover` avoids per-row handler props.
// sidebar tree. Delegated `pointerover` avoids threading a hover callback
// through the memoized row tree.
function SidebarHoverThreadPrewarmer() {
const [prewarmThreadKey, setPrewarmThreadKey] = useState<string | null>(null);

Expand Down
17 changes: 0 additions & 17 deletions apps/web/src/composerDraftStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1815,14 +1815,6 @@ function migratePersistedComposerDraftStoreState(
};
}

// Runs on every store update (each prompt keystroke); drafts are immutable
// snapshots, so unchanged drafts reuse their previously partialized form
// instead of re-cloning nested contexts/annotations/comments per update.
const partializedDraftCache = new WeakMap<
ComposerThreadDraftState,
PersistedComposerThreadDraftState | null
>();

function partializeComposerDraftStoreState(
state: ComposerDraftStoreState,
): PersistedComposerDraftStoreState {
Expand All @@ -1833,16 +1825,7 @@ function partializeComposerDraftStoreState(
if (typeof threadKey !== "string" || threadKey.length === 0) {
continue;
}
const cached = partializedDraftCache.get(draft);
if (cached !== undefined) {
if (cached !== null) {
persistedDraftsByThreadKey[threadKey] =
cached as DeepMutable<PersistedComposerThreadDraftState>;
}
continue;
}
const persisted = partializeComposerThreadDraft(draft);
partializedDraftCache.set(draft, persisted);
if (persisted !== null) {
persistedDraftsByThreadKey[threadKey] =
persisted as DeepMutable<PersistedComposerThreadDraftState>;
Expand Down
66 changes: 20 additions & 46 deletions apps/web/src/connection/useDesktopLocalBootstraps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DesktopEnvironmentBootstrap } from "@t3tools/contracts";
import { useSyncExternalStore } from "react";
import { useEffect, useState } from "react";

import { readDesktopSecondaryBootstraps } from "./desktopLocal";

Expand All @@ -26,53 +26,27 @@ function bootstrapsEqual(
});
}

// One shared poller for all consumers (sidebar, command palette, ...): a single
// interval runs only while someone is subscribed, and listeners are notified
// only when the topology actually changed — each poll returns a fresh array,
// so publishing it unconditionally would re-render every consumer per tick.
const listeners = new Set<() => void>();
let currentSnapshot: ReadonlyArray<DesktopEnvironmentBootstrap> | null = null;
let pollInterval: ReturnType<typeof setInterval> | null = null;

function poll(): void {
const next = readDesktopSecondaryBootstraps();
if (currentSnapshot !== null && bootstrapsEqual(currentSnapshot, next)) {
return;
}
currentSnapshot = next;
for (const listener of listeners) {
listener();
}
}

function subscribe(listener: () => void): () => void {
listeners.add(listener);
if (pollInterval === null) {
poll();
pollInterval = setInterval(poll, DESKTOP_LOCAL_BOOTSTRAP_POLL_MS);
}
return () => {
listeners.delete(listener);
if (listeners.size === 0 && pollInterval !== null) {
clearInterval(pollInterval);
pollInterval = null;
}
};
}

function getSnapshot(): ReadonlyArray<DesktopEnvironmentBootstrap> {
// First read happens during render, before subscribe() has polled.
currentSnapshot ??= readDesktopSecondaryBootstraps();
return currentSnapshot;
}

/**
* Reactively track the desktop's secondary local backends (e.g. a parallel WSL
* backend). The bridge exposes no change event, so we re-read on an interval;
* failed reads retain the latest successful snapshot, while a successful empty
* read clears it. Use this instead of polling the bridge ad hoc so every
* renderer consumer reads the same topology.
* backend). The bridge exposes no change event, so each hook instance re-reads
* on its own interval; a poll returns a fresh array, so the previous reference
* is kept when the topology is unchanged to avoid re-rendering the consumer
* every tick. Use this instead of polling the bridge ad hoc.
*/
export function useDesktopLocalBootstraps(): ReadonlyArray<DesktopEnvironmentBootstrap> {
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
const [bootstraps, setBootstraps] = useState<ReadonlyArray<DesktopEnvironmentBootstrap>>(
readDesktopSecondaryBootstraps,
);

useEffect(() => {
const read = () => {
const next = readDesktopSecondaryBootstraps();
setBootstraps((previous) => (bootstrapsEqual(previous, next) ? previous : next));
};
read();
const interval = setInterval(read, DESKTOP_LOCAL_BOOTSTRAP_POLL_MS);
return () => clearInterval(interval);
}, []);

return bootstraps;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
43 changes: 3 additions & 40 deletions apps/web/src/session-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1366,42 +1366,6 @@ function compareActivityLifecycleRank(kind: string): number {
return 1;
}

function isSortedByCreatedAt(rows: ReadonlyArray<TimelineEntry>): boolean {
for (let index = 1; index < rows.length; index += 1) {
if (rows[index - 1]!.createdAt.localeCompare(rows[index]!.createdAt) > 0) {
return false;
}
}
return true;
}

// Stable k-way merge of per-kind rows that are each already in createdAt order
// (the reducers maintain that invariant). Equal timestamps keep the same order
// a stable sort of the concatenation would produce.
function mergeTimelineRows(sources: ReadonlyArray<ReadonlyArray<TimelineEntry>>): TimelineEntry[] {
const merged: TimelineEntry[] = [];
const cursors = sources.map(() => 0);
const total = sources.reduce((sum, rows) => sum + rows.length, 0);
while (merged.length < total) {
let nextSource = -1;
for (let index = 0; index < sources.length; index += 1) {
const candidate = sources[index]![cursors[index]!];
if (candidate === undefined) {
continue;
}
if (
nextSource === -1 ||
candidate.createdAt.localeCompare(sources[nextSource]![cursors[nextSource]!]!.createdAt) < 0
) {
nextSource = index;
}
}
merged.push(sources[nextSource]![cursors[nextSource]!]!);
cursors[nextSource] = cursors[nextSource]! + 1;
}
return merged;
}

export function deriveTimelineEntries(
messages: ReadonlyArray<ChatMessage>,
proposedPlans: ReadonlyArray<ProposedPlan>,
Expand All @@ -1425,10 +1389,9 @@ export function deriveTimelineEntries(
createdAt: entry.createdAt,
entry,
}));
const sources = [messageRows, proposedPlanRows, workRows];
return sources.every(isSortedByCreatedAt)
? mergeTimelineRows(sources)
: sources.flat().toSorted((a, b) => a.createdAt.localeCompare(b.createdAt));
return [...messageRows, ...proposedPlanRows, ...workRows].toSorted((a, b) =>
a.createdAt.localeCompare(b.createdAt),
);
}

export function inferCheckpointTurnCountByTurnId(
Expand Down
Loading