Skip to content

Commit cf07d06

Browse files
t3dotggjuliusmarmingecodex
authored
Extract independent web cleanup from mobile stack (#2855)
Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com>
1 parent 3126894 commit cf07d06

13 files changed

Lines changed: 138 additions & 20 deletions

apps/web/src/components/ChatView.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1408,8 +1408,9 @@ export default function ChatView(props: ChatViewProps) {
14081408
if (previewUrls.length === 0) return;
14091409

14101410
const previousPreviewUrls = attachmentPreviewHandoffByMessageIdRef.current[messageId] ?? [];
1411+
const nextPreviewUrlSet = new Set(previewUrls);
14111412
for (const previewUrl of previousPreviewUrls) {
1412-
if (!previewUrls.includes(previewUrl)) {
1413+
if (!nextPreviewUrlSet.has(previewUrl)) {
14131414
revokeBlobPreviewUrl(previewUrl);
14141415
}
14151416
}

apps/web/src/components/Sidebar.logic.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ export function resolveThreadRowClassName(input: {
300300
isSelected: boolean;
301301
}): string {
302302
const baseClassName =
303-
"h-7 w-full translate-x-0 cursor-pointer justify-start px-2 text-left select-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring";
303+
"h-6 w-full translate-x-0 cursor-pointer justify-start px-2 text-left select-none focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring sm:h-7";
304304

305305
if (input.isSelected && input.isActive) {
306306
return cn(

apps/web/src/components/Sidebar.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -680,11 +680,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
680680
render={
681681
<span
682682
aria-label={threadEnvironmentLabel ?? "Remote"}
683-
className="inline-flex h-5 items-center justify-center"
683+
className="inline-flex items-center justify-center"
684684
/>
685685
}
686686
>
687-
<CloudIcon className="block size-3 text-muted-foreground/60" />
687+
<CloudIcon className="size-3 text-muted-foreground/40" />
688688
</TooltipTrigger>
689689
<TooltipPopup side="top">{threadEnvironmentLabel}</TooltipPopup>
690690
</Tooltip>
@@ -809,7 +809,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList(
809809
return (
810810
<SidebarMenuSub
811811
ref={attachThreadListAutoAnimateRef}
812-
className="mx-1 my-0 w-full translate-x-0 gap-0.5 overflow-hidden px-1.5 py-0"
812+
className="mx-0.5 my-0 w-full translate-x-0 gap-0.5 overflow-hidden px-1 py-0 sm:mx-1 sm:px-1.5"
813813
>
814814
{shouldShowThreadPanel && showEmptyThreadState ? (
815815
<SidebarMenuSubItem className="w-full" data-thread-selection-safe>

apps/web/src/components/chat/ChatHeader.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,17 +89,20 @@ export const ChatHeader = memo(function ChatHeader({
8989
});
9090

9191
return (
92-
<div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2">
93-
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden sm:gap-3">
92+
<div className="@container/header-actions flex min-w-0 flex-1 flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
93+
<div className="flex min-w-0 flex-wrap items-center gap-2 overflow-hidden sm:flex-1 sm:flex-nowrap sm:gap-3">
9494
<SidebarTrigger className="size-7 shrink-0 md:hidden" />
9595
<h2
96-
className="min-w-0 shrink truncate text-sm font-medium text-foreground"
96+
className="min-w-0 flex-1 basis-40 truncate text-sm font-medium text-foreground"
9797
title={activeThreadTitle}
9898
>
9999
{activeThreadTitle}
100100
</h2>
101101
{activeProjectName && (
102-
<Badge variant="outline" className="min-w-0 shrink overflow-hidden">
102+
<Badge
103+
variant="outline"
104+
className="min-w-0 max-w-full shrink overflow-hidden sm:max-w-56"
105+
>
103106
<span className="min-w-0 truncate">{activeProjectName}</span>
104107
</Badge>
105108
)}
@@ -109,7 +112,7 @@ export const ChatHeader = memo(function ChatHeader({
109112
</Badge>
110113
)}
111114
</div>
112-
<div className="flex shrink-0 items-center justify-end gap-2 @3xl/header-actions:gap-3">
115+
<div className="flex min-w-0 flex-wrap items-center justify-start gap-2 sm:shrink-0 sm:justify-end @3xl/header-actions:gap-3">
113116
{activeProjectScripts && (
114117
<ProjectScriptsControl
115118
scripts={activeProjectScripts}

apps/web/src/components/chat/OpenInPicker.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor
146146
value: "file-manager",
147147
},
148148
];
149-
return baseOptions.filter((option) => availableEditors.includes(option.value));
149+
const availableEditorSet = new Set(availableEditors);
150+
return baseOptions.filter((option) => availableEditorSet.has(option.value));
150151
};
151152

152153
export const OpenInPicker = memo(function OpenInPicker({

apps/web/src/lib/contextWindow.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,23 @@ describe("contextWindow", () => {
4444
expect(snapshot).toBeNull();
4545
});
4646

47+
it("keeps valid zero-usage snapshots", () => {
48+
const snapshot = deriveLatestContextWindowSnapshot([
49+
makeActivity("activity-1", "context-window.updated", {
50+
usedTokens: 0,
51+
maxTokens: 100_000,
52+
}),
53+
]);
54+
55+
expect(snapshot).toMatchObject({
56+
usedTokens: 0,
57+
maxTokens: 100_000,
58+
remainingTokens: 100_000,
59+
usedPercentage: 0,
60+
remainingPercentage: 100,
61+
});
62+
});
63+
4764
it("formats compact token counts", () => {
4865
expect(formatContextWindowTokens(999)).toBe("999");
4966
expect(formatContextWindowTokens(1400)).toBe("1.4k");

apps/web/src/lib/contextWindow.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function deriveLatestContextWindowSnapshot(
3636

3737
const payload = asRecord(activity.payload);
3838
const usedTokens = asFiniteNumber(payload?.usedTokens);
39-
if (usedTokens === null || usedTokens <= 0) {
39+
if (usedTokens === null || usedTokens < 0) {
4040
continue;
4141
}
4242

apps/web/src/lib/lruCache.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,21 @@ describe("LRUCache", () => {
4040
expect(cache.get("b")).toBe("B");
4141
expect(cache.get("c")).toBe("C");
4242
});
43+
44+
it("does not cache entries larger than the memory budget", () => {
45+
const cache = new LRUCache<string>(2, 25);
46+
cache.set("a", "A", 10);
47+
cache.set("oversized", "X", 30);
48+
49+
expect(cache.get("a")).toBe("A");
50+
expect(cache.get("oversized")).toBeNull();
51+
});
52+
53+
it("preserves an existing entry when an oversized replacement is rejected", () => {
54+
const cache = new LRUCache<string>(2, 25);
55+
cache.set("a", "A", 10);
56+
cache.set("a", "oversized", 30);
57+
58+
expect(cache.get("a")).toBe("A");
59+
});
4360
});

apps/web/src/lib/lruCache.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ export class LRUCache<T> {
2323
}
2424

2525
set(key: string, value: T, approximateSize: number): void {
26+
if (approximateSize > this.maxMemoryBytes) {
27+
return;
28+
}
29+
2630
const existing = this.cache.get(key);
2731
if (existing) {
2832
this.totalSize -= existing.approximateSize;

apps/web/src/lib/threadSort.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,31 @@ describe("sortThreads", () => {
114114
]);
115115
});
116116

117+
it("falls back to createdAt when updatedAt is invalid", () => {
118+
const sorted = sortThreads(
119+
[
120+
makeThread({
121+
id: ThreadId.make("thread-1"),
122+
createdAt: "2026-03-09T10:00:00.000Z",
123+
updatedAt: "invalid-date" as never,
124+
messages: [],
125+
}),
126+
makeThread({
127+
id: ThreadId.make("thread-2"),
128+
createdAt: "2026-03-09T09:00:00.000Z",
129+
updatedAt: "2026-03-09T09:30:00.000Z",
130+
messages: [],
131+
}),
132+
],
133+
"updated_at",
134+
);
135+
136+
expect(sorted.map((thread) => thread.id)).toEqual([
137+
ThreadId.make("thread-1"),
138+
ThreadId.make("thread-2"),
139+
]);
140+
});
141+
117142
it("falls back to id ordering when threads have no sortable timestamps", () => {
118143
const sorted = sortThreads(
119144
[
@@ -162,6 +187,29 @@ describe("sortThreads", () => {
162187
]);
163188
});
164189

190+
it("uses updatedAt as a fallback for created_at sorting when createdAt is invalid", () => {
191+
const sorted = sortThreads(
192+
[
193+
makeThread({
194+
id: ThreadId.make("thread-1"),
195+
createdAt: "invalid-date" as never,
196+
updatedAt: "2026-03-09T10:05:00.000Z",
197+
}),
198+
makeThread({
199+
id: ThreadId.make("thread-2"),
200+
createdAt: "2026-03-09T10:00:00.000Z",
201+
updatedAt: "2026-03-09T10:10:00.000Z",
202+
}),
203+
],
204+
"created_at",
205+
);
206+
207+
expect(sorted.map((thread) => thread.id)).toEqual([
208+
ThreadId.make("thread-1"),
209+
ThreadId.make("thread-2"),
210+
]);
211+
});
212+
165213
it("returns the latest active thread for a project", () => {
166214
const latestThread = getLatestThreadForProject(
167215
[

0 commit comments

Comments
 (0)