Skip to content

Commit c661abe

Browse files
committed
chore: merge main into model picker layout fix
2 parents 401b119 + 54c5f25 commit c661abe

314 files changed

Lines changed: 9197 additions & 2458 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/triage/PLAYBOOK.md‎

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,16 @@ of `main` for that work, never the tag-pinned diagnosis clone.
125125
- Show the user the complete final issue text and get an explicit yes before
126126
posting. Never post without it.
127127
- Note at the end of the issue which model and agent produced it.
128-
- If `gh` is not authenticated, offer `gh auth login`, or build a prefilled
129-
https://github.com/pingdotgg/t3code/issues/new URL with title and body query
130-
parameters; print the URL, and open it in their browser only after they
131-
approve.
128+
- Post with `gh issue create --repo pingdotgg/t3code --label via-triage` only
129+
if `gh api repos/pingdotgg/t3code --jq .permissions.triage` prints `true`.
130+
- Otherwise, or if `gh` is not authenticated, build a prefilled
131+
https://github.com/pingdotgg/t3code/issues/new?template=via-triage.yml URL.
132+
The form applies the label whoever submits it. Add `title` plus one
133+
URL-encoded query parameter per form field id: `what-happened`, `diagnosis`,
134+
`steps`, `version`, `environment`, `evidence`, `related`, `workaround` and
135+
`agent`. Keep the full encoded URL below 6,000 characters by shortening any
136+
fields as needed. Give the user any omitted text to paste into the form before
137+
submitting. Print the URL, and open it in their browser only after they approve.
132138
- If the user pasted screenshots, remind them to drag the images into the issue
133139
after it is created; they cannot be attached from here.
134140

‎apps/desktop/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@t3tools/desktop",
3-
"version": "0.1.5",
3+
"version": "0.1.6",
44
"private": true,
55
"type": "module",
66
"main": "dist-electron/boot.cjs",

‎apps/desktop/src/preview/Manager.test.ts‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,12 @@ describe("previewWindowOpenAction", () => {
8686
).toBe("popup");
8787
});
8888

89+
it("opens HTTP links in preview tabs", () => {
90+
for (const disposition of ["foreground-tab", "background-tab"] as const) {
91+
expect(PreviewManager.previewWindowOpenAction(details({ disposition }))).toBe("new-tab");
92+
}
93+
});
94+
8995
it("keeps a form POST with a body on the in-place path", () => {
9096
const postBody = { data: [], contentType: "application/x-www-form-urlencoded" };
9197
expect(
@@ -98,15 +104,32 @@ describe("previewWindowOpenAction", () => {
98104

99105
it("does not hand a window to schemes that cannot be hardened", () => {
100106
for (const url of [
101-
"javascript:alert(1)",
102-
"file:///etc/passwd",
103107
"vscode://vscode-remote/ssh-remote+box/tmp",
104-
"not a url",
108+
"blob:https://example.com/0b6c6a5e-55d4-4b4c-9d47-8f1b0c1d2e3f",
105109
]) {
106110
expect(PreviewManager.previewWindowOpenAction(details({ url }))).toBe("navigate");
107111
}
108112
});
109113

114+
it("denies local and inline schemes that no page could open in place itself", () => {
115+
for (const url of [
116+
"data:text/html,<h1>hi</h1>",
117+
"DaTa:text/html,<h1>hi</h1>",
118+
"file:///etc/passwd",
119+
"javascript:alert(1)",
120+
"view-source:https://example.com",
121+
"chrome://gpu",
122+
"about:srcdoc",
123+
"devtools://devtools/bundled/inspector.html",
124+
"filesystem:https://example.com/temporary/page.html",
125+
"not a url",
126+
]) {
127+
for (const disposition of ["new-window", "foreground-tab", "background-tab"] as const) {
128+
expect(PreviewManager.previewWindowOpenAction(details({ url, disposition }))).toBe("deny");
129+
}
130+
}
131+
});
132+
110133
it("denies a blank window instead of blanking the opener", () => {
111134
for (const url of ["about:blank", ""]) {
112135
for (const disposition of ["new-window", "foreground-tab"] as const) {

‎apps/desktop/src/preview/Manager.ts‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -423,13 +423,17 @@ type OpenLinkListener = (event: DesktopPreviewOpenLinkEvent) => Effect.Effect<vo
423423
// about:blank inherits relaxed guest preferences; external editor schemes must reach the OS handler.
424424
const POPUP_PROTOCOLS = new Set(["http:", "https:"]);
425425

426-
const isPopupUrl = (rawUrl: string): boolean => {
427-
try {
428-
return POPUP_PROTOCOLS.has(new URL(rawUrl).protocol);
429-
} catch {
430-
return false;
431-
}
432-
};
426+
// Main-process loadURL bypasses Chromium's renderer-side restrictions on local and inline URLs.
427+
const IN_PLACE_DENIED_PROTOCOLS = new Set([
428+
"about:",
429+
"chrome:",
430+
"data:",
431+
"devtools:",
432+
"file:",
433+
"filesystem:",
434+
"javascript:",
435+
"view-source:",
436+
]);
433437

434438
/** Popups bypass will-attach-webview hardening and would inherit the picker's contextIsolation:false. */
435439
const POPUP_WINDOW_OPTIONS = {
@@ -440,15 +444,19 @@ const POPUP_WINDOW_OPTIONS = {
440444
},
441445
} satisfies Electron.BrowserWindowConstructorOptions;
442446

443-
/** OAuth needs a surviving popup opener; unhardenable schemes and form POSTs must stay in place. */
447+
/**
448+
* OAuth needs a surviving popup opener. `blob:`, app links (behind the session's `openExternal`
449+
* prompt) and form POSTs stay in place.
450+
*/
444451
export const previewWindowOpenAction = (details: {
445452
readonly url: string;
446453
readonly disposition: Electron.HandlerDetails["disposition"];
447454
readonly postBody?: Electron.PostBody;
448455
}): "popup" | "new-tab" | "navigate" | "deny" => {
456+
const protocol = URL.parse(details.url)?.protocol;
449457
// MSAL redirects on a denied blank popup; navigating the tab would destroy that fallback.
450-
if (details.url === "" || details.url === "about:blank") return "deny";
451-
if (!isPopupUrl(details.url)) return "navigate";
458+
if (protocol === undefined || IN_PLACE_DENIED_PROTOCOLS.has(protocol)) return "deny";
459+
if (!POPUP_PROTOCOLS.has(protocol)) return "navigate";
452460
if (details.disposition === "new-window") return "popup";
453461
return !details.postBody &&
454462
(details.disposition === "foreground-tab" || details.disposition === "background-tab")

‎apps/desktop/src/window/DesktopWindow.test.ts‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ function makeFakeBrowserWindow() {
7878
zoomLevel = level;
7979
}),
8080
isLoadingMainFrame: vi.fn(() => false),
81+
navigationHistory: {
82+
canGoBack: vi.fn(() => true),
83+
canGoForward: vi.fn(() => true),
84+
goBack: vi.fn(),
85+
goForward: vi.fn(),
86+
},
8187
on: vi.fn((eventName: string, listener: (...args: readonly unknown[]) => void) => {
8288
webContentsListeners.set(eventName, listener);
8389
}),
@@ -136,6 +142,7 @@ function makeFakeBrowserWindow() {
136142
reload: webContents.reload,
137143
reloadIgnoringCache: webContents.reloadIgnoringCache,
138144
toggleDevTools: webContents.toggleDevTools,
145+
navigationHistory: webContents.navigationHistory,
139146
send: webContents.send,
140147
setZoomLevel: webContents.setZoomLevel,
141148
setWindowButtonPosition: window.setWindowButtonPosition,
@@ -934,6 +941,39 @@ describe("DesktopWindow", () => {
934941
}),
935942
);
936943

944+
it.effect("maps macOS horizontal swipes to available history navigation", () =>
945+
Effect.gen(function* () {
946+
const fakeWindow = makeFakeBrowserWindow();
947+
const createCount = yield* Ref.make(0);
948+
const mainWindow = yield* Ref.make<Option.Option<Electron.BrowserWindow>>(Option.none());
949+
const layer = layerTest({ window: fakeWindow.window, createCount, mainWindow });
950+
951+
yield* Effect.gen(function* () {
952+
const desktopWindow = yield* DesktopWindow.DesktopWindow;
953+
yield* desktopWindow.handleBackendReady(new URL("http://127.0.0.1:3773"));
954+
const swipe = fakeWindow.windowListeners.get("swipe");
955+
assert.isDefined(swipe);
956+
if (!swipe) return;
957+
const history = fakeWindow.navigationHistory;
958+
959+
swipe({}, "left");
960+
assert.equal(history.goBack.mock.calls.length, 1);
961+
assert.equal(history.goForward.mock.calls.length, 0);
962+
swipe({}, "right");
963+
assert.equal(history.goForward.mock.calls.length, 1);
964+
965+
swipe({}, "up");
966+
swipe({}, "down");
967+
history.canGoBack.mockReturnValue(false);
968+
history.canGoForward.mockReturnValue(false);
969+
swipe({}, "left");
970+
swipe({}, "right");
971+
assert.equal(history.goBack.mock.calls.length, 1);
972+
assert.equal(history.goForward.mock.calls.length, 1);
973+
}).pipe(Effect.provide(layer));
974+
}),
975+
);
976+
937977
it.effect("persists the current main window bounds before the window closes", () =>
938978
Effect.gen(function* () {
939979
const fakeWindow = makeFakeBrowserWindow();

‎apps/desktop/src/window/DesktopWindow.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,12 @@ export const make = Effect.gen(function* () {
685685
});
686686

687687
if (environment.platform === "darwin") {
688+
// Mouse drivers can deliver Back/Forward as macOS swipe gestures instead of buttons.
689+
window.on("swipe", (_event, direction) => {
690+
const history = window.webContents.navigationHistory;
691+
if (direction === "left" && history.canGoBack()) history.goBack();
692+
else if (direction === "right" && history.canGoForward()) history.goForward();
693+
});
688694
window.on("enter-full-screen", () => {
689695
window.webContents.send(WINDOW_FULLSCREEN_STATE_CHANNEL, true);
690696
});

‎apps/mobile/app.config.test.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { afterEach, expect, it, vi } from "vite-plus/test";
2+
3+
// Keep config parsing independent of local env files and build credentials.
4+
vi.mock("../../scripts/lib/public-config.ts", () => ({
5+
loadRepoEnv: () => ({ APP_VARIANT: process.env.APP_VARIANT }),
6+
}));
7+
8+
afterEach(() => {
9+
vi.unstubAllEnvs();
10+
});
11+
12+
async function loadConfig(variant: string | undefined) {
13+
vi.resetModules();
14+
vi.stubEnv("APP_VARIANT", variant);
15+
return (await import("./app.config.ts")).default;
16+
}
17+
18+
it.each(["prod", ""])("rejects APP_VARIANT=%j before selecting production IDs", async (value) => {
19+
await expect(loadConfig(value)).rejects.toThrow(
20+
"APP_VARIANT must be development, preview, or production.",
21+
);
22+
});
23+
24+
it.each([
25+
[undefined, "com.t3tools.t3code"],
26+
["development", "com.t3tools.t3code.dev"],
27+
["preview", "com.t3tools.t3code.preview"],
28+
["production", "com.t3tools.t3code"],
29+
])("keeps the iOS and Android identifiers for APP_VARIANT=%j", async (value, identifier) => {
30+
const config = await loadConfig(value);
31+
expect(config.ios?.bundleIdentifier).toBe(identifier);
32+
expect(config.android?.package).toBe(identifier);
33+
});

‎apps/mobile/app.config.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,10 @@ function resolveAppVariant(value: string | undefined): AppVariant {
108108
case "preview":
109109
case "production":
110110
return value;
111-
default:
111+
case undefined:
112112
return "production";
113+
default:
114+
throw new Error("APP_VARIANT must be development, preview, or production.");
113115
}
114116
}
115117

‎apps/mobile/modules/t3-native-controls/expo-module.config.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"modules": ["T3NativeControlsModule", "T3KeyboardCommandsModule"],
55
"appDelegateSubscribers": [
66
"T3WorkspaceOrientationSubscriber",
7-
"T3MenuDismissTouchGateSubscriber"
7+
"T3MenuDismissTouchGateSubscriber",
8+
"T3BackgroundGraceSubscriber"
89
]
910
},
1011
"android": {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import ExpoModulesCore
2+
import UIKit
3+
4+
public final class T3BackgroundGraceSubscriber: ExpoAppDelegateSubscriber {
5+
private var task = UIBackgroundTaskIdentifier.invalid
6+
7+
public func applicationWillResignActive(_ application: UIApplication) {
8+
guard task == .invalid else { return }
9+
task = application.beginBackgroundTask(withName: "T3 connection grace") { [weak self] in
10+
self?.endTask(application)
11+
}
12+
}
13+
14+
public func applicationDidBecomeActive(_ application: UIApplication) {
15+
endTask(application)
16+
}
17+
18+
private func endTask(_ application: UIApplication) {
19+
guard task != .invalid else { return }
20+
application.endBackgroundTask(task)
21+
task = .invalid
22+
}
23+
}

0 commit comments

Comments
 (0)