Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
0450f85
fix(muse): a declined /compact reports its reason and no longer wedge…
juliusmarminge Nov 8, 2026
57ded10
fix(web): restore bold weight in rendered Markdown (#25694)
juliusmarminge Nov 8, 2026
f475de2
fix(web): device tool update buttons fit their popovers (#27030)
juliusmarminge Nov 8, 2026
ecf7f1a
fix(web): mobile sidebar keeps its width when shelves expand (#27541)
juliusmarminge Nov 8, 2026
28eff11
fix(web): lineage phase headers truncate instead of overflowing the d…
juliusmarminge Nov 8, 2026
71c2d3a
fix(server): storage report lists project worktrees nested inside pla…
juliusmarminge Nov 8, 2026
a1af184
fix(scripts): report:renders starts on Windows without a vp shim (#32…
only21mil Nov 8, 2026
8a84e94
perf(server): skip oversized history during handoff selection (#23821)
AksharP5 Nov 8, 2026
7b680b6
feat(antigravity): support agy CLI as custom binaryPath (#32503)
jorotodorovv Nov 8, 2026
e11c2fd
fix(mobile): share PDFs from the Android attachment viewer (#32266)
shivamhwp Nov 8, 2026
3cd97a1
fix(server): saved preview screenshots keep full resolution (#28255)
TonybynMp4 Nov 8, 2026
20fcc6c
fix(server): refresh linking tools in existing agent sessions (#26737)
shivamhwp Nov 8, 2026
d6c2b09
feat(server): html tools take a file path, so agents stop retyping pa…
t3dotgg Nov 8, 2026
09a54f0
fix(search): keep whole-word title matches above busy prefixes (#32242)
shivamhwp Nov 8, 2026
dff0b11
fix(connect): show why relay unlink failed (#25198)
shivamhwp Nov 8, 2026
22dc6f9
fix(mobile): keep scheduled task prompts clear of microphone (#32498)
shivamhwp Nov 8, 2026
8e6a732
feat(web,mobile): move a scheduled task to another thread (#31379)
binjamshid Nov 8, 2026
af65a55
perf(server): Codex tool calls no longer wait on reasoning summaries …
t3dotgg Nov 8, 2026
6417151
fix(web): fill combobox search fields (#32926)
shivamhwp Nov 8, 2026
ade4730
fix(server): mise-installed providers update through mise instead of …
ryanrhughes Nov 8, 2026
0d809db
perf(sidebar): avoid full git status for branch labels (#32947)
StiensWout Nov 8, 2026
4013183
perf(desktop): speed up cold Linux x64 backend startup (#32987)
StiensWout Nov 8, 2026
23e3465
fix(clients): unloaded thread links show their title and project icon…
Yash-Singh1 Nov 8, 2026
f558007
fix(clients): parse angle-bracketed thread link destinations (#30709)
ScriptedAlchemy Nov 8, 2026
3da48c1
fix(desktop): prepare server bytecode on Linux ARM64 (#33050)
Yash-Singh1 Nov 8, 2026
ea9fbc1
fix(desktop): open cursor:// and vscode:// file links from chat (#23486)
fixfon Nov 8, 2026
564b2f9
perf(server): batch worktree branch allocation and avoid path collisi…
StiensWout Nov 8, 2026
b76a595
perf(server): skip unnecessary worktree cleanup scans (#32933)
StiensWout Nov 8, 2026
36d0dcb
fix(server): wake idle parents for late delegated results (#32573)
StiensWout Nov 8, 2026
a61cdb5
fix(mobile): keep side chats readable over the conversation (#32240)
PixPMusic Nov 8, 2026
bc92973
fix(desktop): declare Calendar access for hosted commands (#21592)
Lenoridev Nov 8, 2026
f92daf1
fix(server): macOS Bash terminals now start as login shells (#22295)
fixfon Nov 8, 2026
9c0baf4
fix(web): keep the wide table scrollbar visible when it overflows (#2…
fixfon Nov 8, 2026
7875604
fix(web): file chip tooltips show the whole path (#27993)
Princeyadav05 Nov 8, 2026
ecea300
fix(web): sidebar search results can be dragged onto the composer (#2…
root-daemon Nov 8, 2026
385851d
fix(server): allow slow Antigravity browser preflights (#25211)
maria-rcks Nov 8, 2026
1d78b33
fix(server): name rejected certificates when linking peers (#28060)
maria-rcks Nov 8, 2026
4549bee
fix(web): terminal types macOS Option-layer characters (#21596)
neominik Nov 8, 2026
3fa57b9
fix(web): boring sidebar threads drag onto the composer again (#26724)
juliusmarminge Nov 8, 2026
7fe1310
fix(web): Copy MCP URL uses the T3 Connect address (#32937)
t3dotgg Nov 8, 2026
e6d4256
fix(web): cmd+shift+o picks the current project before pinned ones (#…
t3dotgg Nov 8, 2026
d456cd7
fix(server): respect gh Unix sockets for GitHub requests (#27733)
voltcrash Nov 8, 2026
9557ab4
perf(mobile): thread list resumes from the saved cache, and large gap…
juliusmarminge Nov 9, 2026
f26623f
feat(mobile): iOS socket compresses traffic (#32288)
juliusmarminge Nov 9, 2026
b7d33af
perf(server): macOS worktrees open 2x faster by cloning a cached chec…
t3dotgg Nov 9, 2026
d5290e9
fix(web): mic button always sits next to send (#33133)
t3dotgg Nov 9, 2026
8969e1d
feat: Settings reclaims freed database space in one click (#32941)
t3dotgg Nov 9, 2026
25868ed
fix(github): hidden GitHub comments and reviews no longer show in T3 …
t3dotgg Nov 9, 2026
9028d76
feat(mobile): long-press an image to save it to Photos (#29598)
t3dotgg Nov 9, 2026
521ac7c
feat(settings): clean up Labs: promote tool calls, remove plan mode a…
t3dotgg Nov 9, 2026
2d76213
Merge upstream v0.1.8-nightly.20261109.4392 into custom/usage-badge
Nov 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 1 addition & 1 deletion .github/workflows/release-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ jobs:
shell: bash
run: |
sudo apt-get update
sudo apt-get install -y libsecret-1-dev pkg-config
sudo apt-get install -y libsecret-1-dev pkg-config libgtk-3-0t64 libnss3 libasound2t64 libgbm1
if ! command -v magick >/dev/null 2>&1 && ! command -v convert >/dev/null 2>&1; then
sudo apt-get install -y imagemagick
fi
Expand Down
61 changes: 61 additions & 0 deletions apps/desktop/src/electron/ElectronShell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,67 @@ describe("ElectronShell", () => {
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("opens local editor file deep links", () =>
Effect.gen(function* () {
openExternalMock.mockResolvedValue(undefined);

const urls = [
"cursor://file/Users/me/repo/file.md:186",
"vscode://file/Users/me/repo/file.md:3:7",
"vscode-insiders://file/Users/me/repo/file.md",
"vscodium://file/C:/Users/me/repo/file.md:3",
];
const electronShell = yield* ElectronShell.ElectronShell;
const results = yield* Effect.forEach(urls, (url) => electronShell.openExternal(url));

assert.deepEqual(results, [true, true, true, true]);
assert.deepEqual(
openExternalMock.mock.calls,
urls.map((url) => [url]),
);
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("normalizes the scheme case of local editor file deep links", () =>
Effect.gen(function* () {
openExternalMock.mockResolvedValue(undefined);

const electronShell = yield* ElectronShell.ElectronShell;
const result = yield* electronShell.openExternal("VSCode://file/Users/me/repo/file.md:3");

assert.equal(result, true);
assert.deepEqual(openExternalMock.mock.calls, [["vscode://file/Users/me/repo/file.md:3"]]);
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("does not open editor URLs that are not absolute local file links", () =>
Effect.gen(function* () {
openExternalMock.mockResolvedValue(undefined);

const electronShell = yield* ElectronShell.ElectronShell;
const results = yield* Effect.forEach(
[
"cursor://user@file/Users/me/repo/file.md",
"cursor://:secret@file/Users/me/repo/file.md",
"cursor://file:8080/Users/me/repo/file.md",
"cursor://anysphere.cursor-deeplink/prompt?text=hi",
"vscode://vscode.git/clone?url=https%3A%2F%2Fexample.com%2Frepo.git",
"cursor://file",
"cursor://file/",
"cursor:file/Users/me/repo/file.md",
"foo://file/Users/me/repo/file.md",
"zed://file/Users/me/repo/file.md",
"jetbrains://file/Users/me/repo/file.md",
"javascript:alert(1)",
],
(url) => electronShell.openExternal(url),
);

assert.deepEqual(results, Array(12).fill(false));
assert.equal(openExternalMock.mock.calls.length, 0);
}).pipe(Effect.provide(ElectronShell.layer)),
);

it.effect("does not open editor URLs that mix up link shapes", () =>
Effect.gen(function* () {
openExternalMock.mockResolvedValue(undefined);
Expand Down
7 changes: 5 additions & 2 deletions apps/desktop/src/electron/ElectronShell.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { MAC_PERMISSION_SETTINGS_URLS } from "../permissions/MacPermission.ts";
import {
isLocalEditorFileUrl,
REMOTE_CAPABLE_EDITOR_IDS,
remoteSchemeForEditor,
type SystemSettingsPane,
Expand All @@ -11,7 +12,7 @@ import * as Option from "effect/Option";

import * as Electron from "electron";

// Remote editor deep links must reach the OS handler.
// Remote editor deep links and local editor file links must reach the OS handler.
const SAFE_WEB_PROTOCOLS = new Set(["http:", "https:"]);
const REMOTE_EDITOR_PROTOCOLS = new Set(
REMOTE_CAPABLE_EDITOR_IDS.flatMap((id) => {
Expand Down Expand Up @@ -44,7 +45,9 @@ export function parseSafeExternalUrl(rawUrl: unknown): Option.Option<string> {

try {
const url = new URL(rawUrl);
return SAFE_WEB_PROTOCOLS.has(url.protocol) || isRemoteEditorUrl(url)
return SAFE_WEB_PROTOCOLS.has(url.protocol) ||
isRemoteEditorUrl(url) ||
isLocalEditorFileUrl(url)
? Option.some(url.href)
: Option.none();
} catch {
Expand Down
2 changes: 0 additions & 2 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ const clientSettings: ClientSettings = {
glassOpacity: 80,
onboardingCompletedAt: null,
panelAnimationDurationMs: 0,
planModeEnabled: false,
proactivePanelsEnabled: true,
showSkillsInSlashMenu: false,
persistComposerContextStrip: true,
Expand All @@ -69,7 +68,6 @@ const clientSettings: ClientSettings = {
boringSidebarMultilineRows: false,
threadDetailsUsageEnabled: false,
threadDetailsItems: {},
legacySidebarEnabled: false,
sidebarWorkingShelfEnabled: false,
loadBalancingEnabled: false,
loadBalancingWeights: { "environment-1": 75, "environment-2": 0 },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package expo.modules.t3nativecontrols

import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.text.format.DateFormat
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import expo.modules.kotlin.Promise
import expo.modules.kotlin.modules.Module
Expand Down Expand Up @@ -44,6 +51,15 @@ class T3NativeControlsModule : Module() {
}
}

AsyncFunction("saveImageToLibrary") { uri: String, mimeType: String, name: String ->
val context = appContext.reactContext ?: error("The app is not active.")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
saveImageToPictures(context, Uri.parse(uri), mimeType, name)
} else {
error("Saving images needs Android 10 or later.")
}
}

OnActivityResult { _, (requestCode) ->
if (requestCode == 7343) {
filePreviewPromise?.resolve(null)
Expand Down Expand Up @@ -84,3 +100,32 @@ class T3NativeControlsModule : Module() {
}
}
}

// Android 10+ lets apps add their own media without a storage permission.
@RequiresApi(Build.VERSION_CODES.Q)
private fun saveImageToPictures(context: Context, source: Uri, mimeType: String, name: String) {
val resolver = context.contentResolver
val values = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, name)
put(MediaStore.Images.Media.MIME_TYPE, mimeType)
put(MediaStore.Images.Media.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/T3 Code")
put(MediaStore.Images.Media.IS_PENDING, 1)
}
val target = resolver.insert(
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
values
) ?: error("Could not create the image in Pictures.")
runCatching {
val input = resolver.openInputStream(source) ?: error("The image is no longer available.")
input.use { from ->
val output = resolver.openOutputStream(target) ?: error("Could not write the image.")
output.use { from.copyTo(it) }
}
resolver.update(
target,
ContentValues().apply { put(MediaStore.Images.Media.IS_PENDING, 0) },
null,
null
)
}.onFailure { resolver.delete(target, null, null) }.getOrThrow()
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import AVFoundation
import ExpoModulesCore
import Photos
import Security
import UIKit
import UniformTypeIdentifiers

@ExpoModule("T3NativeControls")
public final class T3NativeControlsModule: Module {
Expand Down Expand Up @@ -123,6 +125,11 @@ public final class T3NativeControlsModule: Module {
AsyncFunction("shareFileFromSource") { (url: URL, title: String, identifier: String, promise: Promise) in
try self.shareFile(url: url, title: title, sourceIdentifier: identifier, promise: promise)
}.runOnQueue(.main)

// Photos names the asset itself, so iOS ignores the Android display name.
AsyncFunction("saveImageToLibrary") { (url: URL, mimeType: String, name: String, promise: Promise) in
Self.saveImageToLibrary(url: url, mimeType: mimeType, name: name, promise: promise)
}
}

// UIKit presenters are main-actor isolated rather than run on the JS thread
Expand Down Expand Up @@ -297,6 +304,35 @@ public final class T3NativeControlsModule: Module {
if filePresentation?.identifier == identifier { filePresentation?.dismiss() }
}

private static func saveImageToLibrary(url: URL, mimeType: String, name: String, promise: Promise) {
PHPhotoLibrary.requestAuthorization(for: .addOnly) { status in
guard status == .authorized || status == .limited else {
promise.reject(NSError(
domain: "T3NativePhotos",
code: 1,
userInfo: [NSLocalizedDescriptionKey: "Allow T3 Code to add photos in Settings."]
))
return
}
PHPhotoLibrary.shared().performChanges({
let options = PHAssetResourceCreationOptions()
options.uniformTypeIdentifier = UTType(mimeType: mimeType)?.identifier
options.originalFilename = name
PHAssetCreationRequest.forAsset().addResource(with: .photo, fileURL: url, options: options)
}, completionHandler: { saved, error in
if saved {
promise.resolve(nil)
} else {
promise.reject(error ?? NSError(
domain: "T3NativePhotos",
code: 2,
userInfo: [NSLocalizedDescriptionKey: "Photos could not save this image."]
))
}
})
}
}

private func shareFile(url: URL, title: String, sourceIdentifier: String, promise: Promise) throws {
guard let presenter = appContext?.utilities?.currentViewController() else {
throw NSError(
Expand Down
4 changes: 4 additions & 0 deletions apps/mobile/modules/t3-web-socket/expo-module.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"platforms": ["apple"],
"apple": { "modules": ["T3WebSocketModule"] }
}
13 changes: 13 additions & 0 deletions apps/mobile/modules/t3-web-socket/ios/T3WebSocket.podspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Pod::Spec.new do |s|
s.name = 'T3WebSocket'
s.version = '1.0.0'
s.summary = 'WebSocket client with permessage-deflate for T3 Code mobile.'
s.description = 'React Native ships SocketRocket on iOS, which never negotiates compression. This module dials the T3 server through URLSessionWebSocketTask, which offers permessage-deflate on its own.'
s.author = 'T3 Tools'
s.homepage = 'https://t3tools.com'
s.platform = :ios, '18.0'
s.source = { :path => '.' }
s.static_framework = true
s.dependency 'ExpoModulesCore'
s.source_files = '*.swift'
end
Loading