| author | |
| committer | |
| log | cf07d0632174b432ce8bd70f3292f615afe8b184 |
| tree | 87eda75cce61ff2c67571664f9887836d0eeeecb |
| parent | 31268945f88a712fad9309d5ea9290cb8496af83 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Julius Marminge <julius0216@outlook.com>
Co-authored-by: codex <codex@users.noreply.github.com>13 files changed, 138 insertions(+), 20 deletions(-)
apps/web/src/components/ChatView.tsx+2-1| ... | @@ -1408,8 +1408,9 @@ export default function ChatView(props: ChatViewProps) { | ... | @@ -1408,8 +1408,9 @@ export default function ChatView(props: ChatViewProps) { |
| 1408 | if (previewUrls.length === 0) return; | 1408 | if (previewUrls.length === 0) return; |
| 1409 | 1409 | ||
| 1410 | const previousPreviewUrls = attachmentPreviewHandoffByMessageIdRef.current[messageId] ?? []; | 1410 | const previousPreviewUrls = attachmentPreviewHandoffByMessageIdRef.current[messageId] ?? []; |
| 1411 | const nextPreviewUrlSet = new Set(previewUrls); | ||
| 1411 | for (const previewUrl of previousPreviewUrls) { | 1412 | for (const previewUrl of previousPreviewUrls) { |
| 1412 | if (!previewUrls.includes(previewUrl)) { | 1413 | if (!nextPreviewUrlSet.has(previewUrl)) { |
| 1413 | revokeBlobPreviewUrl(previewUrl); | 1414 | revokeBlobPreviewUrl(previewUrl); |
| 1414 | } | 1415 | } |
| 1415 | } | 1416 | } |
apps/web/src/components/Sidebar.logic.ts+1-1| ... | @@ -300,7 +300,7 @@ export function resolveThreadRowClassName(input: { | ... | @@ -300,7 +300,7 @@ export function resolveThreadRowClassName(input: { |
| 300 | isSelected: boolean; | 300 | isSelected: boolean; |
| 301 | }): string { | 301 | }): string { |
| 302 | const baseClassName = | 302 | 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"; |
| 304 | 304 | ||
| 305 | if (input.isSelected && input.isActive) { | 305 | if (input.isSelected && input.isActive) { |
| 306 | return cn( | 306 | return cn( |
apps/web/src/components/Sidebar.tsx+3-3| ... | @@ -680,11 +680,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP | ... | @@ -680,11 +680,11 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP |
| 680 | render={ | 680 | render={ |
| 681 | <span | 681 | <span |
| 682 | aria-label={threadEnvironmentLabel ?? "Remote"} | 682 | aria-label={threadEnvironmentLabel ?? "Remote"} |
| 683 | className="inline-flex h-5 items-center justify-center" | 683 | className="inline-flex items-center justify-center" |
| 684 | /> | 684 | /> |
| 685 | } | 685 | } |
| 686 | > | 686 | > |
| 687 | <CloudIcon className="block size-3 text-muted-foreground/60" /> | 687 | <CloudIcon className="size-3 text-muted-foreground/40" /> |
| 688 | </TooltipTrigger> | 688 | </TooltipTrigger> |
| 689 | <TooltipPopup side="top">{threadEnvironmentLabel}</TooltipPopup> | 689 | <TooltipPopup side="top">{threadEnvironmentLabel}</TooltipPopup> |
| 690 | </Tooltip> | 690 | </Tooltip> |
| ... | @@ -809,7 +809,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList( | ... | @@ -809,7 +809,7 @@ const SidebarProjectThreadList = memo(function SidebarProjectThreadList( |
| 809 | return ( | 809 | return ( |
| 810 | <SidebarMenuSub | 810 | <SidebarMenuSub |
| 811 | ref={attachThreadListAutoAnimateRef} | 811 | 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" |
| 813 | > | 813 | > |
| 814 | {shouldShowThreadPanel && showEmptyThreadState ? ( | 814 | {shouldShowThreadPanel && showEmptyThreadState ? ( |
| 815 | <SidebarMenuSubItem className="w-full" data-thread-selection-safe> | 815 | <SidebarMenuSubItem className="w-full" data-thread-selection-safe> |
apps/web/src/components/chat/ChatHeader.tsx+8-5| ... | @@ -89,17 +89,20 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -89,17 +89,20 @@ export const ChatHeader = memo(function ChatHeader({ |
| 89 | }); | 89 | }); |
| 90 | 90 | ||
| 91 | return ( | 91 | return ( |
| 92 | <div className="@container/header-actions flex min-w-0 flex-1 items-center gap-2"> | 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-1 items-center gap-2 overflow-hidden sm:gap-3"> | 93 | <div className="flex min-w-0 flex-wrap items-center gap-2 overflow-hidden sm:flex-1 sm:flex-nowrap sm:gap-3"> |
| 94 | <SidebarTrigger className="size-7 shrink-0 md:hidden" /> | 94 | <SidebarTrigger className="size-7 shrink-0 md:hidden" /> |
| 95 | <h2 | 95 | <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" |
| 97 | title={activeThreadTitle} | 97 | title={activeThreadTitle} |
| 98 | > | 98 | > |
| 99 | {activeThreadTitle} | 99 | {activeThreadTitle} |
| 100 | </h2> | 100 | </h2> |
| 101 | {activeProjectName && ( | 101 | {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 | > | ||
| 103 | <span className="min-w-0 truncate">{activeProjectName}</span> | 106 | <span className="min-w-0 truncate">{activeProjectName}</span> |
| 104 | </Badge> | 107 | </Badge> |
| 105 | )} | 108 | )} |
| ... | @@ -109,7 +112,7 @@ export const ChatHeader = memo(function ChatHeader({ | ... | @@ -109,7 +112,7 @@ export const ChatHeader = memo(function ChatHeader({ |
| 109 | </Badge> | 112 | </Badge> |
| 110 | )} | 113 | )} |
| 111 | </div> | 114 | </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"> |
| 113 | {activeProjectScripts && ( | 116 | {activeProjectScripts && ( |
| 114 | <ProjectScriptsControl | 117 | <ProjectScriptsControl |
| 115 | scripts={activeProjectScripts} | 118 | scripts={activeProjectScripts} |
apps/web/src/components/chat/OpenInPicker.tsx+2-1| ... | @@ -146,7 +146,8 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor | ... | @@ -146,7 +146,8 @@ const resolveOptions = (platform: string, availableEditors: ReadonlyArray<Editor |
| 146 | value: "file-manager", | 146 | value: "file-manager", |
| 147 | }, | 147 | }, |
| 148 | ]; | 148 | ]; |
| 149 | return baseOptions.filter((option) => availableEditors.includes(option.value)); | 149 | const availableEditorSet = new Set(availableEditors); |
| 150 | return baseOptions.filter((option) => availableEditorSet.has(option.value)); | ||
| 150 | }; | 151 | }; |
| 151 | 152 | ||
| 152 | export const OpenInPicker = memo(function OpenInPicker({ | 153 | export const OpenInPicker = memo(function OpenInPicker({ |
apps/web/src/lib/contextWindow.test.ts+17| ... | @@ -44,6 +44,23 @@ describe("contextWindow", () => { | ... | @@ -44,6 +44,23 @@ describe("contextWindow", () => { |
| 44 | expect(snapshot).toBeNull(); | 44 | expect(snapshot).toBeNull(); |
| 45 | }); | 45 | }); |
| 46 | 46 | ||
| 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 | |||
| 47 | it("formats compact token counts", () => { | 64 | it("formats compact token counts", () => { |
| 48 | expect(formatContextWindowTokens(999)).toBe("999"); | 65 | expect(formatContextWindowTokens(999)).toBe("999"); |
| 49 | expect(formatContextWindowTokens(1400)).toBe("1.4k"); | 66 | expect(formatContextWindowTokens(1400)).toBe("1.4k"); |
apps/web/src/lib/contextWindow.ts+1-1| ... | @@ -36,7 +36,7 @@ export function deriveLatestContextWindowSnapshot( | ... | @@ -36,7 +36,7 @@ export function deriveLatestContextWindowSnapshot( |
| 36 | 36 | ||
| 37 | const payload = asRecord(activity.payload); | 37 | const payload = asRecord(activity.payload); |
| 38 | const usedTokens = asFiniteNumber(payload?.usedTokens); | 38 | const usedTokens = asFiniteNumber(payload?.usedTokens); |
| 39 | if (usedTokens === null || usedTokens <= 0) { | 39 | if (usedTokens === null || usedTokens < 0) { |
| 40 | continue; | 40 | continue; |
| 41 | } | 41 | } |
| 42 | 42 |
apps/web/src/lib/lruCache.test.ts+17| ... | @@ -40,4 +40,21 @@ describe("LRUCache", () => { | ... | @@ -40,4 +40,21 @@ describe("LRUCache", () => { |
| 40 | expect(cache.get("b")).toBe("B"); | 40 | expect(cache.get("b")).toBe("B"); |
| 41 | expect(cache.get("c")).toBe("C"); | 41 | expect(cache.get("c")).toBe("C"); |
| 42 | }); | 42 | }); |
| 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 | }); | ||
| 43 | }); | 60 | }); |
apps/web/src/lib/lruCache.ts+4| ... | @@ -23,6 +23,10 @@ export class LRUCache<T> { | ... | @@ -23,6 +23,10 @@ export class LRUCache<T> { |
| 23 | } | 23 | } |
| 24 | 24 | ||
| 25 | set(key: string, value: T, approximateSize: number): void { | 25 | set(key: string, value: T, approximateSize: number): void { |
| 26 | if (approximateSize > this.maxMemoryBytes) { | ||
| 27 | return; | ||
| 28 | } | ||
| 29 | |||
| 26 | const existing = this.cache.get(key); | 30 | const existing = this.cache.get(key); |
| 27 | if (existing) { | 31 | if (existing) { |
| 28 | this.totalSize -= existing.approximateSize; | 32 | this.totalSize -= existing.approximateSize; |
apps/web/src/lib/threadSort.test.ts+48| ... | @@ -114,6 +114,31 @@ describe("sortThreads", () => { | ... | @@ -114,6 +114,31 @@ describe("sortThreads", () => { |
| 114 | ]); | 114 | ]); |
| 115 | }); | 115 | }); |
| 116 | 116 | ||
| 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 | |||
| 117 | it("falls back to id ordering when threads have no sortable timestamps", () => { | 142 | it("falls back to id ordering when threads have no sortable timestamps", () => { |
| 118 | const sorted = sortThreads( | 143 | const sorted = sortThreads( |
| 119 | [ | 144 | [ |
| ... | @@ -162,6 +187,29 @@ describe("sortThreads", () => { | ... | @@ -162,6 +187,29 @@ describe("sortThreads", () => { |
| 162 | ]); | 187 | ]); |
| 163 | }); | 188 | }); |
| 164 | 189 | ||
| 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 | |||
| 165 | it("returns the latest active thread for a project", () => { | 213 | it("returns the latest active thread for a project", () => { |
| 166 | const latestThread = getLatestThreadForProject( | 214 | const latestThread = getLatestThreadForProject( |
| 167 | [ | 215 | [ |
apps/web/src/lib/threadSort.ts+15-2| ... | @@ -13,6 +13,17 @@ export function toSortableTimestamp(iso: string | undefined): number | null { | ... | @@ -13,6 +13,17 @@ export function toSortableTimestamp(iso: string | undefined): number | null { |
| 13 | return Number.isFinite(ms) ? ms : null; | 13 | return Number.isFinite(ms) ? ms : null; |
| 14 | } | 14 | } |
| 15 | 15 | ||
| 16 | function getFirstSortableTimestamp(...values: Array<string | null | undefined>): number | null { | ||
| 17 | for (const value of values) { | ||
| 18 | const timestamp = toSortableTimestamp(value ?? undefined); | ||
| 19 | if (timestamp !== null) { | ||
| 20 | return timestamp; | ||
| 21 | } | ||
| 22 | } | ||
| 23 | |||
| 24 | return null; | ||
| 25 | } | ||
| 26 | |||
| 16 | function getLatestUserMessageTimestamp(thread: ThreadSortInput): number { | 27 | function getLatestUserMessageTimestamp(thread: ThreadSortInput): number { |
| 17 | if (thread.latestUserMessageAt) { | 28 | if (thread.latestUserMessageAt) { |
| 18 | return toSortableTimestamp(thread.latestUserMessageAt) ?? Number.NEGATIVE_INFINITY; | 29 | return toSortableTimestamp(thread.latestUserMessageAt) ?? Number.NEGATIVE_INFINITY; |
| ... | @@ -34,7 +45,7 @@ function getLatestUserMessageTimestamp(thread: ThreadSortInput): number { | ... | @@ -34,7 +45,7 @@ function getLatestUserMessageTimestamp(thread: ThreadSortInput): number { |
| 34 | return latestUserMessageTimestamp; | 45 | return latestUserMessageTimestamp; |
| 35 | } | 46 | } |
| 36 | 47 | ||
| 37 | return toSortableTimestamp(thread.updatedAt ?? thread.createdAt) ?? Number.NEGATIVE_INFINITY; | 48 | return getFirstSortableTimestamp(thread.updatedAt, thread.createdAt) ?? Number.NEGATIVE_INFINITY; |
| 38 | } | 49 | } |
| 39 | 50 | ||
| 40 | export function getThreadSortTimestamp( | 51 | export function getThreadSortTimestamp( |
| ... | @@ -42,7 +53,9 @@ export function getThreadSortTimestamp( | ... | @@ -42,7 +53,9 @@ export function getThreadSortTimestamp( |
| 42 | sortOrder: SidebarThreadSortOrder | Exclude<SidebarProjectSortOrder, "manual">, | 53 | sortOrder: SidebarThreadSortOrder | Exclude<SidebarProjectSortOrder, "manual">, |
| 43 | ): number { | 54 | ): number { |
| 44 | if (sortOrder === "created_at") { | 55 | if (sortOrder === "created_at") { |
| 45 | return toSortableTimestamp(thread.createdAt) ?? Number.NEGATIVE_INFINITY; | 56 | return ( |
| 57 | getFirstSortableTimestamp(thread.createdAt, thread.updatedAt) ?? Number.NEGATIVE_INFINITY | ||
| 58 | ); | ||
| 46 | } | 59 | } |
| 47 | return getLatestUserMessageTimestamp(thread); | 60 | return getLatestUserMessageTimestamp(thread); |
| 48 | } | 61 | } |
apps/web/src/uiStateStore.ts+13-2| ... | @@ -63,6 +63,7 @@ const initialState: UiState = { | ... | @@ -63,6 +63,7 @@ const initialState: UiState = { |
| 63 | const persistedCollapsedProjectCwds = new Set<string>(); | 63 | const persistedCollapsedProjectCwds = new Set<string>(); |
| 64 | const persistedExpandedProjectCwds = new Set<string>(); | 64 | const persistedExpandedProjectCwds = new Set<string>(); |
| 65 | const persistedProjectOrderCwds: string[] = []; | 65 | const persistedProjectOrderCwds: string[] = []; |
| 66 | const persistedProjectOrderCwdSet = new Set<string>(); | ||
| 66 | // Pre-fix persisted shape only listed expanded cwds, so anything not listed | 67 | // Pre-fix persisted shape only listed expanded cwds, so anything not listed |
| 67 | // was treated as collapsed. Track whether the loaded blob carried the new | 68 | // was treated as collapsed. Track whether the loaded blob carried the new |
| 68 | // `collapsedProjectCwds` field so we can preserve that legacy semantic for | 69 | // `collapsedProjectCwds` field so we can preserve that legacy semantic for |
| ... | @@ -140,6 +141,7 @@ export function hydratePersistedProjectState(parsed: PersistedUiState): void { | ... | @@ -140,6 +141,7 @@ export function hydratePersistedProjectState(parsed: PersistedUiState): void { |
| 140 | persistedCollapsedProjectCwds.clear(); | 141 | persistedCollapsedProjectCwds.clear(); |
| 141 | persistedExpandedProjectCwds.clear(); | 142 | persistedExpandedProjectCwds.clear(); |
| 142 | persistedProjectOrderCwds.length = 0; | 143 | persistedProjectOrderCwds.length = 0; |
| 144 | persistedProjectOrderCwdSet.clear(); | ||
| 143 | persistedProjectStateUsesLegacyShape = !Array.isArray(parsed.collapsedProjectCwds); | 145 | persistedProjectStateUsesLegacyShape = !Array.isArray(parsed.collapsedProjectCwds); |
| 144 | for (const cwd of parsed.collapsedProjectCwds ?? []) { | 146 | for (const cwd of parsed.collapsedProjectCwds ?? []) { |
| 145 | if (typeof cwd === "string" && cwd.length > 0) { | 147 | if (typeof cwd === "string" && cwd.length > 0) { |
| ... | @@ -152,7 +154,8 @@ export function hydratePersistedProjectState(parsed: PersistedUiState): void { | ... | @@ -152,7 +154,8 @@ export function hydratePersistedProjectState(parsed: PersistedUiState): void { |
| 152 | } | 154 | } |
| 153 | } | 155 | } |
| 154 | for (const cwd of parsed.projectOrderCwds ?? []) { | 156 | for (const cwd of parsed.projectOrderCwds ?? []) { |
| 155 | if (typeof cwd === "string" && cwd.length > 0 && !persistedProjectOrderCwds.includes(cwd)) { | 157 | if (typeof cwd === "string" && cwd.length > 0 && !persistedProjectOrderCwdSet.has(cwd)) { |
| 158 | persistedProjectOrderCwdSet.add(cwd); | ||
| 156 | persistedProjectOrderCwds.push(cwd); | 159 | persistedProjectOrderCwds.push(cwd); |
| 157 | } | 160 | } |
| 158 | } | 161 | } |
| ... | @@ -254,14 +257,22 @@ export function syncProjects(state: UiState, projects: readonly SyncProjectInput | ... | @@ -254,14 +257,22 @@ export function syncProjects(state: UiState, projects: readonly SyncProjectInput |
| 254 | currentLogicalKeyByPhysicalKey.set(project.key, project.logicalKey); | 257 | currentLogicalKeyByPhysicalKey.set(project.key, project.logicalKey); |
| 255 | } | 258 | } |
| 256 | currentProjectCwdsByLogicalKey.clear(); | 259 | currentProjectCwdsByLogicalKey.clear(); |
| 260 | const currentProjectCwdSetsByLogicalKey = new Map<string, Set<string>>(); | ||
| 257 | for (const project of projects) { | 261 | for (const project of projects) { |
| 258 | const cwds = currentProjectCwdsByLogicalKey.get(project.logicalKey); | 262 | const cwds = currentProjectCwdsByLogicalKey.get(project.logicalKey); |
| 259 | if (cwds) { | 263 | if (cwds) { |
| 260 | if (!cwds.includes(project.cwd)) { | 264 | let cwdSet = currentProjectCwdSetsByLogicalKey.get(project.logicalKey); |
| 265 | if (!cwdSet) { | ||
| 266 | cwdSet = new Set(cwds); | ||
| 267 | currentProjectCwdSetsByLogicalKey.set(project.logicalKey, cwdSet); | ||
| 268 | } | ||
| 269 | if (!cwdSet.has(project.cwd)) { | ||
| 270 | cwdSet.add(project.cwd); | ||
| 261 | cwds.push(project.cwd); | 271 | cwds.push(project.cwd); |
| 262 | } | 272 | } |
| 263 | } else { | 273 | } else { |
| 264 | currentProjectCwdsByLogicalKey.set(project.logicalKey, [project.cwd]); | 274 | currentProjectCwdsByLogicalKey.set(project.logicalKey, [project.cwd]); |
| 275 | currentProjectCwdSetsByLogicalKey.set(project.logicalKey, new Set([project.cwd])); | ||
| 265 | } | 276 | } |
| 266 | } | 277 | } |
| 267 | // Build reverse map: for each new logical key, which previous logical keys | 278 | // Build reverse map: for each new logical key, which previous logical keys |
packages/shared/src/observability.test.ts+7-4| ... | @@ -1,9 +1,11 @@ | ... | @@ -1,9 +1,11 @@ |
| 1 | import { assert, describe, it } from "@effect/vitest"; | 1 | import { assert, describe, it } from "@effect/vitest"; |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 3 | import * as Arr from "effect/Array"; | ||
| 3 | import * as Effect from "effect/Effect"; | 4 | import * as Effect from "effect/Effect"; |
| 4 | import * as FileSystem from "effect/FileSystem"; | 5 | import * as FileSystem from "effect/FileSystem"; |
| 5 | import * as Layer from "effect/Layer"; | 6 | import * as Layer from "effect/Layer"; |
| 6 | import * as Logger from "effect/Logger"; | 7 | import * as Logger from "effect/Logger"; |
| 8 | import * as Order from "effect/Order"; | ||
| 7 | import * as Path from "effect/Path"; | 9 | import * as Path from "effect/Path"; |
| 8 | import * as References from "effect/References"; | 10 | import * as References from "effect/References"; |
| 9 | import * as Schema from "effect/Schema"; | 11 | import * as Schema from "effect/Schema"; |
| ... | @@ -170,12 +172,13 @@ describe("observability", () => { | ... | @@ -170,12 +172,13 @@ describe("observability", () => { |
| 170 | } | 172 | } |
| 171 | yield* sink.close(); | 173 | yield* sink.close(); |
| 172 | 174 | ||
| 173 | const matchingFiles = (yield* fileSystem.readDirectory(tempDir)) | 175 | const matchingFiles = Arr.sort( |
| 174 | .filter( | 176 | (yield* fileSystem.readDirectory(tempDir)).filter( |
| 175 | (entry) => | 177 | (entry) => |
| 176 | entry === "shared.trace.ndjson" || entry.startsWith("shared.trace.ndjson."), | 178 | entry === "shared.trace.ndjson" || entry.startsWith("shared.trace.ndjson."), |
| 177 | ) | 179 | ), |
| 178 | .toSorted(); | 180 | Order.String, |
| 181 | ); | ||
| 179 | 182 | ||
| 180 | assert.equal( | 183 | assert.equal( |
| 181 | matchingFiles.some((entry) => entry === "shared.trace.ndjson.1"), | 184 | matchingFiles.some((entry) => entry === "shared.trace.ndjson.1"), |