Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
36caf20
feat(web): choose the default diff file state (#11484)
maria-rcks Sep 13, 2026
68c2277
feat(composer): fold large pastes into text attachments (#11442)
chrisdeeming Sep 13, 2026
6cdbf76
feat(web): expose each chat message as a heading for screen readers (…
Leos-Khai Sep 13, 2026
2db675a
fix(usage): respect provider account homes (#11485)
maria-rcks Sep 13, 2026
2587c80
feat(web): switch saved environments off instead of removing them (#1…
t3dotgg Sep 13, 2026
5781e2b
fix(mobile): stop crashing on launch when a thread has a PR stack (#1…
juliusmarminge Sep 13, 2026
af0657e
fix(mobile): stop alerting that shared content vanished after sending…
juliusmarminge Sep 13, 2026
0e0ddae
feat(web): add opt-in thread notifications and sounds (#11481)
maria-rcks Sep 13, 2026
c299764
fix(server): open Cursor links in classic IDE mode (#11498)
Yash-Singh1 Sep 13, 2026
6fd68f5
feat(source-control): support Forgejo and Gitea with fj and tea (#11436)
maria-rcks Sep 13, 2026
0c5771d
fix(web): match draft row heights to thread rows (#11512)
Yash-Singh1 Sep 13, 2026
3138f57
fix(grok): emit task lifecycle for monitors and background shells (#9…
Svyk Sep 13, 2026
46140c9
fix(web): unify panel resizing and retain final drag width (#11529)
maria-rcks Sep 13, 2026
2ec59ca
fix(web): hide back button for single linked pull requests (#11520)
maria-rcks Sep 13, 2026
21d53ca
fix(files): browse ignored files and load folders on demand (#11527)
maria-rcks Sep 13, 2026
d7c71f9
feat(web): float the pull request comment composer (#11531)
maria-rcks Sep 13, 2026
8b3ddf5
fix(mobile): stop crashing on launch before the shell snapshot arrive…
juliusmarminge Sep 13, 2026
db6e053
feat(github): route pull request operations across matching accounts …
maria-rcks Sep 13, 2026
20a8f1d
chore(mobile): enable noUncheckedIndexedAccess and noImplicitOverride…
juliusmarminge Sep 13, 2026
0a91b9a
feat(mobile): show startup crashes in Settings → Diagnostics (#11540)
juliusmarminge Sep 13, 2026
17f8e2a
feat(mobile): add pooled subscription usage widgets (#11506)
MatthewFeroz Sep 13, 2026
20363c3
feat(web): add provider selector to pull request toolbar (#11524)
maria-rcks Sep 13, 2026
44a6ad2
fix(web): open pull requests in the browser instead of the side panel
Fryuni Sep 13, 2026
47d7540
chore: merge upstream/main into fork
Fryuni Sep 13, 2026
377e1b5
Merge branch 't3code/fix-view-pr-fork-origin' into t3code/merge-upstr…
Fryuni Sep 13, 2026
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
1 change: 1 addition & 0 deletions apps/desktop/src/app/DesktopConnectionCatalogStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ const migrateSavedEnvironmentRecords = Effect.fn(
profiles,
credentials,
remoteDpopTokens: [],
disabledEnvironmentIds: [],
};
});

Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/ipc/DesktopIpcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
openExternal,
openSystemSettings,
checkSystemPermission,
pasteAsText,
probeRemoteEditors,
pickFolder,
pickProjectFavicon,
Expand Down Expand Up @@ -124,6 +125,7 @@ export const installDesktopIpcHandlers = Effect.fn("desktop.ipc.installHandlers"
yield* ipc.handle(openExternal);
yield* ipc.handle(openSystemSettings);
yield* ipc.handle(checkSystemPermission);
yield* ipc.handle(pasteAsText);
yield* ipc.handle(probeRemoteEditors);
yield* ipc.handle(getUpdateState);
yield* ipc.handle(setUpdateChannel);
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/ipc/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const OPEN_EXTERNAL_CHANNEL = "desktop:open-external";
export const OPEN_SYSTEM_SETTINGS_CHANNEL = "desktop:open-system-settings";
export const PROBE_REMOTE_EDITORS_CHANNEL = "desktop:probe-remote-editors";
export const MENU_ACTION_CHANNEL = "desktop:menu-action";
export const PASTE_AS_TEXT_CHANNEL = "desktop:paste-as-text";
export const SNAP_SHOT_EVENT_CHANNEL = "desktop:snap-shot-event";
export const QUIT_SHORTCUT_CHANNEL = "desktop:quit-shortcut";
export const GET_WINDOW_FULLSCREEN_STATE_CHANNEL = "desktop:get-window-fullscreen-state";
Expand Down
54 changes: 54 additions & 0 deletions apps/desktop/src/ipc/methods/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@ import { vi } from "vite-plus/test";

import type * as Electron from "electron";

const { focusedWebContents, ownerWindow } = vi.hoisted(() => ({
focusedWebContents: vi.fn(),
ownerWindow: vi.fn(),
}));
vi.mock("electron", () => ({
webContents: { getFocusedWebContents: focusedWebContents },
BrowserWindow: { fromWebContents: ownerWindow },
}));

import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts";
import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts";
import * as ElectronDialog from "../../electron/ElectronDialog.ts";
import * as ElectronWindow from "../../electron/ElectronWindow.ts";
import {
getLocalEnvironmentBootstraps,
getWindowFullscreenState,
pasteAsText,
pickProjectFavicon,
} from "./window.ts";

Expand Down Expand Up @@ -153,6 +163,50 @@ describe("getWindowFullscreenState", () => {
});
});

describe("pasteAsText", () => {
it.effect(
"pastes into the focused guest only after the main renderer acknowledges the menu action",
() => {
const paste = vi.fn();
const mainPaste = vi.fn();
const window = {
webContents: { id: 42, paste: mainPaste },
isDestroyed: () => false,
} as unknown as Electron.BrowserWindow;
focusedWebContents.mockReturnValue({ paste, isDestroyed: () => false });
ownerWindow.mockReturnValue(window);

return Effect.gen(function* () {
yield* pasteAsText.handler(undefined, { sender: { id: 42 } });
assert.equal(paste.mock.calls.length, 1);
assert.equal(mainPaste.mock.calls.length, 0);

yield* pasteAsText.handler(undefined, { sender: { id: 99 } });
assert.equal(paste.mock.calls.length, 1);
ownerWindow.mockReturnValue({}); // A focused PiP/other BrowserWindow.
yield* pasteAsText.handler(undefined, { sender: { id: 42 } });
assert.equal(paste.mock.calls.length, 1);
ownerWindow.mockReturnValue(null); // Detached contents.
yield* pasteAsText.handler(undefined, { sender: { id: 42 } });
assert.equal(paste.mock.calls.length, 1);
ownerWindow.mockReturnValue(window);
focusedWebContents.mockReturnValue({ paste, isDestroyed: () => true });
yield* pasteAsText.handler(undefined, { sender: { id: 42 } });
assert.equal(paste.mock.calls.length, 1);
focusedWebContents.mockReturnValue(null);
yield* pasteAsText.handler(undefined, { sender: { id: 42 } });
assert.equal(paste.mock.calls.length, 1);
}).pipe(
Effect.provide(
Layer.mock(ElectronWindow.ElectronWindow)({
main: Effect.succeed(Option.some(window)),
}),
),
);
},
);
});

describe("pickProjectFavicon", () => {
it.effect("opens a single-image picker from the project directory", () =>
Effect.gen(function* () {
Expand Down
26 changes: 26 additions & 0 deletions apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,32 @@ export const probeRemoteEditors = DesktopIpc.makeIpcMethod({
}),
});

export const pasteAsText = DesktopIpc.makeIpcMethod({
channel: IpcChannels.PASTE_AS_TEXT_CHANNEL,
payload: Schema.Undefined,
result: Schema.Void,
handler: Effect.fn("desktop.ipc.window.pasteAsText")(function* (_input, event) {
const electronWindow = yield* ElectronWindow.ElectronWindow;
const window = yield* electronWindow.main;
if (
event === undefined ||
Option.isNone(window) ||
window.value.isDestroyed() ||
window.value.webContents.id !== event.sender.id
) {
return;
}
const focused = Electron.webContents.getFocusedWebContents();
if (
focused &&
!focused.isDestroyed() &&
Electron.BrowserWindow.fromWebContents(focused) === window.value
) {
focused.paste();
}
}),
});

/** Theme files are a few KB; anything larger returns empty text and lets the
* renderer reject it by size without the contents ever crossing the bridge. */
const PICKED_THEME_FILE_MAX_BYTES = 256 * 1024;
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ contextBridge.exposeInMainWorld("desktopBridge", {
openSystemSettings: (pane: string) =>
ipcRenderer.invoke(IpcChannels.OPEN_SYSTEM_SETTINGS_CHANNEL, pane),
probeRemoteEditors: () => ipcRenderer.invoke(IpcChannels.PROBE_REMOTE_EDITORS_CHANNEL, undefined),
pasteAsText: () => ipcRenderer.invoke(IpcChannels.PASTE_AS_TEXT_CHANNEL, undefined),
onMenuAction: (listener) => {
const wrappedListener = (_event: Electron.IpcRendererEvent, action: unknown) => {
if (typeof action !== "string") return;
Expand Down
2 changes: 2 additions & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as DesktopClientSettings from "./DesktopClientSettings.ts";

const clientSettings: ClientSettings = {
...DEFAULT_CLIENT_SETTINGS,
notificationMode: "notifications-and-sound",
appearanceContrast: 100,
browserDefaultViewport: { _tag: "preset", width: 1024, height: 600, presetId: "nest-hub" },
browserDefaultZoomFactor: 1.25,
Expand All @@ -34,6 +35,7 @@ const clientSettings: ClientSettings = {
contextWindowMeterEnabled: false,
composerCollapseOnScroll: true,
dismissedProviderUpdateNotificationKeys: [],
diffFilesCollapsed: true,
diffIgnoreWhitespace: true,
diffLayout: "stacked",
environmentIdentificationMode: "artwork",
Expand Down
30 changes: 30 additions & 0 deletions apps/desktop/src/window/DesktopApplicationMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,36 @@ describe("DesktopApplicationMenu", () => {
}),
);

it.effect("owns Paste as Text and routes it through the renderer", () =>
Effect.gen(function* () {
const selectedAction = yield* Deferred.make<string>();
const applicationMenuTemplate =
yield* Deferred.make<readonly Electron.MenuItemConstructorOptions[]>();

yield* configureMenu(selectedAction, applicationMenuTemplate);

const template = yield* Deferred.await(applicationMenuTemplate);
const editMenu = template.find((item) => item.label === "Edit");
assert.isDefined(editMenu);
if (!Array.isArray(editMenu.submenu)) {
throw new Error("Expected Edit menu submenu to be an array.");
}
const pasteAsTextItem = editMenu.submenu.find((item) => item.label === "Paste as Text");
assert.isDefined(pasteAsTextItem);
assert.equal(pasteAsTextItem.accelerator, "CmdOrCtrl+Shift+V");
if (typeof pasteAsTextItem.click !== "function") {
throw new Error("Expected Paste as Text menu item to have a click handler.");
}

pasteAsTextItem.click(
{} as Electron.MenuItem,
{} as Electron.BrowserWindow,
{} as KeyboardEvent,
);
assert.equal(yield* Deferred.await(selectedAction), "paste-as-text");
}),
);

// Zoom must route through DesktopWindow.zoomMain instead of the Electron
// zoom roles: the roles zoom whichever webContents has focus, which breaks
// app zoom while an embedded preview WebContentsView holds focus.
Expand Down
36 changes: 34 additions & 2 deletions apps/desktop/src/window/DesktopApplicationMenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,9 @@ const dispatchMenuAction = Effect.fn("desktop.menu.dispatchMenuAction")(function
action: string,
): Effect.fn.Return<void, DesktopWindow.DesktopWindowError, DesktopWindow.DesktopWindow> {
const desktopWindow = yield* DesktopWindow.DesktopWindow;
yield* desktopWindow.dispatchMenuAction(action);
yield* desktopWindow.dispatchMenuAction(action, {
reveal: action !== "paste-as-text",
});
});

const zoomMainWindow = Effect.fn("desktop.menu.zoomMainWindow")(function* (
Expand Down Expand Up @@ -135,6 +137,9 @@ export const make = Effect.gen(function* () {
const settingsClick = () => {
runMenuEffect("open-settings", dispatchMenuAction("open-settings"));
};
const pasteAsTextClick = () => {
runMenuEffect("paste-as-text", dispatchMenuAction("paste-as-text"));
};
const zoomClick = (direction: DesktopWindow.MainWindowZoomDirection) => () => {
runMenuEffect(`zoom-${direction}`, zoomMainWindow(direction));
};
Expand Down Expand Up @@ -184,7 +189,34 @@ export const make = Effect.gen(function* () {
{ role: environment.platform === "darwin" ? "close" : "quit" },
],
},
{ role: "editMenu" },
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
{
label: "Paste as Text",
accelerator: "CmdOrCtrl+Shift+V",
click: pasteAsTextClick,
},
{ role: "delete" },
{ type: "separator" },
{ role: "selectAll" },
...(environment.platform === "darwin"
? [
{ type: "separator" as const },
{
label: "Speech",
submenu: [{ role: "startSpeaking" as const }, { role: "stopSpeaking" as const }],
},
]
: []),
],
},
{
label: "View",
submenu: [
Expand Down
39 changes: 39 additions & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,45 @@ const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [
// frequent-updates entitlement iOS throttles the update budget sooner.
frequentUpdates: true,
widgets: [
{
name: "SubscriptionUsage",
displayName: "Subscription usage",
description: "Subscription quotas from your connected T3 Code environments.",
configuration: {
title: "Subscription usage",
description:
"Both shows Session and Weekly when available. The Lock Screen shows the tightest selected limit.",
parameters: {
codexPeriod: {
title: "Codex limits",
type: "enum",
default: "auto",
values: [
{ name: "Both", value: "auto" },
{ name: "Session", value: "session" },
{ name: "Weekly", value: "weekly" },
],
},
claudePeriod: {
title: "Claude limits",
type: "enum",
default: "auto",
values: [
{ name: "Both", value: "auto" },
{ name: "Session", value: "session" },
{ name: "Weekly", value: "weekly" },
],
},
},
},
supportedFamilies: [
"systemSmall",
"systemMedium",
"systemLarge",
"systemExtraLarge",
"accessoryRectangular",
],
},
{
name: "AgentActivity",
displayName: "Agent Activity",
Expand Down
12 changes: 12 additions & 0 deletions apps/mobile/modules/t3-composer-editor/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ android {
namespace 'expo.modules.t3composereditor'
compileSdk rootProject.ext.compileSdkVersion

testOptions {
unitTests.includeAndroidResources = true
}

defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
Expand All @@ -17,4 +21,12 @@ android {
dependencies {
implementation project(':expo-modules-core')
implementation project(':t3tools-mobile-markdown-text')
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.robolectric:robolectric:4.16.1'
}

tasks.withType(Test).configureEach {
javaLauncher = javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,12 @@ class T3ComposerEditorModule : Module() {
Prop("spellCheck") { view: T3ComposerEditorView, spellCheck: Boolean ->
view.setSpellCheck(spellCheck)
}
Prop("textPasteThresholdBytes") { view: T3ComposerEditorView, threshold: Int ->
view.setTextPasteThresholdBytes(threshold)
}
Prop("maxInputChars") { view: T3ComposerEditorView, maxInputChars: Int ->
view.setMaxInputChars(maxInputChars)
}

Events(
"onComposerChange",
Expand All @@ -125,6 +131,7 @@ class T3ComposerEditorModule : Module() {
"onComposerPasteImages",
"onComposerContextPress",
"onComposerPasteContext",
"onComposerPasteText",
"onComposerContentSizeChange",
)

Expand Down
Loading