Skip to content

Commit 91c53b5

Browse files
Merge remote-tracking branch 'origin/main' into fix/issue-27170
2 parents 9be0530 + b3fdf2b commit 91c53b5

83 files changed

Lines changed: 3738 additions & 334 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,16 @@ jobs:
140140
- name: Start installing browser secret helper build libraries
141141
if: needs.changes.outputs.build != 'false'
142142
run: |
143-
sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
143+
# Fetch only the indexes these main and universe packages need. The full set is ~42 MB,
144+
# which a slow mirror can stretch past the wait below.
145+
sudo sed -i -e 's|http://|https://|g' -e 's/^Components: .*/Components: main universe/' \
146+
/etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
144147
(
145148
set +e
146-
sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config xvfb
149+
sudo apt-get update -o Acquire::Languages=none \
150+
-o Acquire::IndexTargets::deb::DEP-11::DefaultEnabled=false \
151+
-o Acquire::IndexTargets::deb::CNF::DefaultEnabled=false &&
152+
sudo apt-get install -y libsecret-1-dev pkg-config xvfb
147153
echo "$?" > "$RUNNER_TEMP/apt-status"
148154
) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null &
149155
@@ -287,10 +293,16 @@ jobs:
287293
# the background while Vite+ installs; the step before the tests waits for it.
288294
- name: Start installing browser secret helper build libraries
289295
run: |
290-
sudo sed -i 's|http://|https://|g' /etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
296+
# Fetch only the indexes these main and universe packages need. The full set is ~42 MB,
297+
# which a slow mirror can stretch past the wait below.
298+
sudo sed -i -e 's|http://|https://|g' -e 's/^Components: .*/Components: main universe/' \
299+
/etc/apt/blacksmith-ubuntu-mirrors.txt /etc/apt/sources.list.d/ubuntu.sources
291300
(
292301
set +e
293-
sudo apt-get update && sudo apt-get install -y libsecret-1-dev pkg-config
302+
sudo apt-get update -o Acquire::Languages=none \
303+
-o Acquire::IndexTargets::deb::DEP-11::DefaultEnabled=false \
304+
-o Acquire::IndexTargets::deb::CNF::DefaultEnabled=false &&
305+
sudo apt-get install -y libsecret-1-dev pkg-config
294306
echo "$?" > "$RUNNER_TEMP/apt-status"
295307
) > "$RUNNER_TEMP/apt.log" 2>&1 < /dev/null &
296308

‎apps/desktop/src/app/DesktopClerk.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ export const make = Effect.gen(function* () {
116116
yield* electronApp.quit;
117117
return yield* Effect.interrupt;
118118
}
119+
// Chromium opens these caches at the first event-loop turn, before ready.
120+
yield* DesktopUserData.clearStaleGpuCaches({
121+
userDataPath,
122+
electronVersion: process.versions.electron,
123+
});
119124

120125
const pendingSecondInstances: Array<readonly string[]> = [];
121126
let handleSecondInstance = (argv: readonly string[]) => {

‎apps/desktop/src/app/DesktopPreReadyFileSystem.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ export const make = FileSystem.makeNoop({
4949
syncFs("makeDirectory", path, () => {
5050
NodeFS.mkdirSync(path, { recursive: options?.recursive ?? false });
5151
}),
52+
remove: (path, options) =>
53+
syncFs("remove", path, () =>
54+
NodeFS.rmSync(path, {
55+
recursive: options?.recursive ?? false,
56+
force: options?.force ?? false,
57+
}),
58+
),
5259
writeFileString: (path, data, options) =>
5360
syncFs("writeFileString", path, () =>
5461
NodeFS.writeFileSync(path, data, { flag: options?.flag ?? "w" }),

‎apps/desktop/src/app/DesktopUserData.test.ts‎

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { assert, it } from "@effect/vitest";
33
import * as Effect from "effect/Effect";
44
import * as FileSystem from "effect/FileSystem";
55
import * as Path from "effect/Path";
6+
import * as PlatformError from "effect/PlatformError";
67

7-
import { resolveUserDataPath } from "./DesktopUserData.ts";
8+
import { clearStaleGpuCaches, resolveUserDataPath } from "./DesktopUserData.ts";
89

910
it.effect.each(["t3code", "T3 Code (Alpha)"])(
1011
"preserves Windows credential keys from %s without copying browser databases",
@@ -40,3 +41,100 @@ it.effect.each(["t3code", "T3 Code (Alpha)"])(
4041
);
4142
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
4243
);
44+
45+
const gpuCacheDirectories = [
46+
"GPUCache",
47+
"GrShaderCache",
48+
"ShaderCache",
49+
"DawnGraphiteCache",
50+
"DawnWebGPUCache",
51+
"GPUPersistentCache",
52+
];
53+
54+
it.effect.each([
55+
{ profile: "written by Electron 44.10.0", marker: "44.10.0" },
56+
{ profile: "with no recorded Electron version", marker: undefined },
57+
])("clears GPU caches $profile once when Electron 44.10.1 starts", ({ marker }) =>
58+
Effect.gen(function* () {
59+
const fs = yield* FileSystem.FileSystem;
60+
const path = yield* Path.Path;
61+
const userDataPath = yield* fs.makeTempDirectoryScoped({ prefix: "t3-v2-gpu-cache-" });
62+
for (const name of gpuCacheDirectories) {
63+
yield* fs.makeDirectory(path.join(userDataPath, name), { recursive: true });
64+
yield* fs.writeFileString(path.join(userDataPath, name, "data_0"), "44.10.0 shaders");
65+
}
66+
yield* fs.makeDirectory(path.join(userDataPath, "Local Storage"), { recursive: true });
67+
yield* fs.writeFileString(path.join(userDataPath, "Local Storage", "LOCK"), "profile");
68+
yield* fs.writeFileString(path.join(userDataPath, "Local State"), "profile");
69+
if (marker !== undefined) {
70+
yield* fs.writeFileString(path.join(userDataPath, "gpu-cache-electron-version"), marker);
71+
}
72+
73+
yield* clearStaleGpuCaches({ userDataPath, electronVersion: "44.10.1" });
74+
75+
const survivingCaches: Array<string> = [];
76+
for (const name of gpuCacheDirectories) {
77+
if (yield* fs.exists(path.join(userDataPath, name))) survivingCaches.push(name);
78+
}
79+
assert.deepStrictEqual(survivingCaches, []);
80+
assert.equal(
81+
yield* fs.readFileString(path.join(userDataPath, "Local Storage", "LOCK")),
82+
"profile",
83+
);
84+
assert.equal(yield* fs.readFileString(path.join(userDataPath, "Local State")), "profile");
85+
86+
yield* fs.makeDirectory(path.join(userDataPath, "GPUCache"));
87+
yield* fs.writeFileString(path.join(userDataPath, "GPUCache", "data_0"), "44.10.1 shaders");
88+
yield* clearStaleGpuCaches({ userDataPath, electronVersion: "44.10.1" });
89+
assert.equal(
90+
yield* fs.readFileString(path.join(userDataPath, "GPUCache", "data_0")),
91+
"44.10.1 shaders",
92+
);
93+
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
94+
);
95+
96+
it.effect("keeps GPU caches built on the first launch of a fresh install", () =>
97+
Effect.gen(function* () {
98+
const fs = yield* FileSystem.FileSystem;
99+
const path = yield* Path.Path;
100+
const appData = yield* fs.makeTempDirectoryScoped({ prefix: "t3-v2-fresh-" });
101+
const userDataPath = path.join(appData, "t3code-v2");
102+
103+
yield* clearStaleGpuCaches({ userDataPath, electronVersion: "44.10.1" });
104+
yield* fs.makeDirectory(path.join(userDataPath, "GPUCache"));
105+
yield* fs.writeFileString(path.join(userDataPath, "GPUCache", "data_0"), "44.10.1 shaders");
106+
yield* clearStaleGpuCaches({ userDataPath, electronVersion: "44.10.1" });
107+
108+
assert.equal(
109+
yield* fs.readFileString(path.join(userDataPath, "GPUCache", "data_0")),
110+
"44.10.1 shaders",
111+
);
112+
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
113+
);
114+
115+
it.effect("retries on the next launch when a GPU cache cannot be removed", () =>
116+
Effect.gen(function* () {
117+
const writes: Array<string> = [];
118+
const fileSystem = FileSystem.makeNoop({
119+
remove: (path) =>
120+
Effect.fail(
121+
PlatformError.systemError({
122+
_tag: "PermissionDenied",
123+
module: "FileSystem",
124+
method: "remove",
125+
pathOrDescriptor: path,
126+
}),
127+
),
128+
writeFileString: (path) =>
129+
Effect.sync(() => {
130+
writes.push(path);
131+
}),
132+
});
133+
134+
yield* clearStaleGpuCaches({ userDataPath: "/profile", electronVersion: "44.10.1" }).pipe(
135+
Effect.provideService(FileSystem.FileSystem, fileSystem),
136+
);
137+
138+
assert.deepStrictEqual(writes, []);
139+
}).pipe(Effect.provide(NodeServices.layer)),
140+
);

‎apps/desktop/src/app/DesktopUserData.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,43 @@ export const resolveUserDataPath = Effect.fn("desktop.userData.resolveUserDataPa
9595
return destinationPath;
9696
},
9797
);
98+
99+
const GPU_CACHE_DIRECTORIES = [
100+
"GPUCache",
101+
"GrShaderCache",
102+
"ShaderCache",
103+
"DawnGraphiteCache",
104+
"DawnWebGPUCache",
105+
"GPUPersistentCache",
106+
];
107+
108+
// Keyed on Electron, not Chromium: Electron patches ship Chromium backports under the same Chrome version.
109+
export const clearStaleGpuCaches = Effect.fn("desktop.userData.clearStaleGpuCaches")(
110+
function* (input: { readonly userDataPath: string; readonly electronVersion: string }) {
111+
const fs = yield* FileSystem.FileSystem;
112+
const path = yield* Path.Path;
113+
const markerPath = path.join(input.userDataPath, "gpu-cache-electron-version");
114+
const cachedVersion = yield* fs.readFileString(markerPath).pipe(
115+
Effect.catchIf(
116+
(error) => error.reason._tag === "NotFound",
117+
() => Effect.undefined,
118+
),
119+
);
120+
if (cachedVersion?.trim() === input.electronVersion) return;
121+
for (const name of GPU_CACHE_DIRECTORIES) {
122+
yield* fs.remove(path.join(input.userDataPath, name), { recursive: true, force: true });
123+
}
124+
yield* fs.makeDirectory(input.userDataPath, { recursive: true });
125+
yield* fs.writeFileString(markerPath, input.electronVersion);
126+
},
127+
(effect, input) =>
128+
effect.pipe(
129+
Effect.catch((error) =>
130+
Effect.logWarning("could not clear stale GPU caches", {
131+
userDataPath: input.userDataPath,
132+
electronVersion: input.electronVersion,
133+
error: error.message,
134+
}),
135+
),
136+
),
137+
);

‎apps/mobile/src/features/pull-requests/PullRequestDiscussionScreen.tsx‎

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import {
2+
parsePullRequestAnchor,
3+
pullRequestAnchorMatchesUrl,
4+
resolvePullRequestReviewAnchor,
5+
} from "@t3tools/shared/pullRequestAnchor";
16
import { AppActivityIndicator } from "../../components/ui/AppActivityIndicator";
27
import { useAtomRefresh } from "@effect/atom-react";
38
import { useFocusEffect, useNavigation, type StaticScreenProps } from "@react-navigation/native";
@@ -8,7 +13,7 @@ import {
813
import { AsyncResult } from "effect/reactivity";
914
import type { PullRequestThreadComment } from "@t3tools/contracts";
1015
import { useRef, useState } from "react";
11-
import { ScrollView, View } from "react-native";
16+
import { ScrollView, View, type ScrollViewInstance } from "react-native";
1217
import { KeyboardAvoidingView, useKeyboardState } from "react-native-keyboard-controller";
1318
import { NativeScrollViewMarker as ScrollViewMarker } from "../../native/NativeScrollViewMarker";
1419
import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -138,11 +143,27 @@ function Discussion({ params }: { params: PullRequestParams & { readonly discuss
138143
[...extra, ...(thread?.comments ?? [])].map((comment) => [comment.id, comment]),
139144
).values(),
140145
].sort((left, right) => left.createdAt.localeCompare(right.createdAt));
146+
const scrollRef = useRef<ScrollViewInstance>(null);
147+
const pendingTop = useRef<number | null>(null);
148+
const positionedComment = useRef<string | null>(null);
149+
const location = parsePullRequestAnchor(params.navigationUrl);
150+
const anchor =
151+
location?.kind === "timeline"
152+
? resolvePullRequestReviewAnchor(location.anchor, [], [{ comments }])
153+
: null;
141154
const canReply = permissions.canReply && Boolean(thread);
142155
const resolveLabel = thread?.isResolved ? "Unresolve" : "Resolve";
143156
return (
144157
<PullRequestMarkdownRepository params={params} changeRequest={detail.data}>
145-
<KeyboardAvoidingView automaticOffset behavior="height" className="flex-1 bg-screen">
158+
<KeyboardAvoidingView
159+
automaticOffset
160+
behavior="height"
161+
className="flex-1 bg-screen"
162+
// Any touch, such as replying or loading replies, ends the wait to reposition the link.
163+
onTouchStart={() => {
164+
pendingTop.current = null;
165+
}}
166+
>
146167
<AppBar
147168
title="Discussion"
148169
subtitle={`#${params.number}`}
@@ -162,6 +183,12 @@ function Discussion({ params }: { params: PullRequestParams & { readonly discuss
162183
/>
163184
<ScrollViewMarker style={{ flex: 1 }}>
164185
<ScrollView
186+
ref={scrollRef}
187+
onContentSizeChange={() => {
188+
if (pendingTop.current === null) return;
189+
scrollRef.current?.scrollTo({ y: pendingTop.current, animated: false });
190+
pendingTop.current = null;
191+
}}
165192
className="flex-1"
166193
contentInsetAdjustmentBehavior="automatic"
167194
keyboardShouldPersistTaps="handled"
@@ -192,7 +219,25 @@ function Discussion({ params }: { params: PullRequestParams & { readonly discuss
192219
<Text>This discussion is no longer available.</Text>
193220
) : null}
194221
{comments.map((comment) => (
195-
<View key={comment.id} className="gap-3 border-b border-border-subtle py-4">
222+
<View
223+
key={comment.id}
224+
onLayout={(event) => {
225+
if (
226+
anchor === null ||
227+
!pullRequestAnchorMatchesUrl(anchor, comment.url) ||
228+
positionedComment.current === comment.id
229+
)
230+
return;
231+
positionedComment.current = comment.id;
232+
pendingTop.current = event.nativeEvent.layout.y;
233+
scrollRef.current?.scrollTo({ y: pendingTop.current, animated: false });
234+
}}
235+
className={
236+
anchor !== null && pullRequestAnchorMatchesUrl(anchor, comment.url)
237+
? "-mx-3 gap-3 rounded-2xl border border-primary bg-primary/10 px-3 py-4"
238+
: "gap-3 border-b border-border-subtle py-4"
239+
}
240+
>
196241
<View className="flex-row items-center gap-2">
197242
<PullRequestAvatar actor={comment.author} size={28} />
198243
<Text className="min-w-0 flex-1 text-sm font-t3-medium">

0 commit comments

Comments
 (0)