Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
f86c5e8
fix(server): skip IDE detection in Claude probes (#8634)
yashranaway Aug 31, 2026
ad38700
chore(macroscope): review diagnostic overrides (#8917)
t3-code[bot] Aug 31, 2026
2921050
fix(contracts): accept CLI event origins (#8905)
nateEc Aug 31, 2026
bba79cc
fix(web): hide invalid slash skill completions (#8904)
nateEc Aug 31, 2026
746c932
fix(mobile): defer draft navigation until submission completes (#8914)
juliusmarminge Aug 31, 2026
4e8e64f
chore: disable CodeRabbit review status (#8933)
juliusmarminge Aug 31, 2026
038bf37
Delete app.json (#8934)
juliusmarminge Aug 31, 2026
35da581
fix(web): show scrollbar for wide markdown tables (#8868)
UtkarshUsername Aug 31, 2026
5ce92c2
fix(mobile): shimmer active tool rows (#8932)
juliusmarminge Aug 31, 2026
4a9d2d0
chore(deps): bump Electron to 43.4.1 (#8626)
StiensWout Aug 31, 2026
ef84bc9
fix(chat): smooth worktree setup status (#8922)
juliusmarminge Aug 31, 2026
31c1c59
feat(mobile): add video playback with native iOS controls (#8919)
juliusmarminge Aug 31, 2026
f47e740
fix(web): prevent chat metadata overlap (#8851)
MatthewFeroz Aug 31, 2026
6d15c5b
fix(server): preserve usage cache outside walked roots (#8540)
Lucenx9 Aug 31, 2026
f8e4acc
feat(mobile): add native image and PDF previews (#8959)
juliusmarminge Aug 31, 2026
41adccc
fix(server): allow long thread IDs in HTTP routes (#8898)
nateEc Aug 31, 2026
929f7e6
fix(shared): preserve Windows shell PATH priority (#8748)
UtkarshUsername Aug 31, 2026
c50b0b4
fix(web): make WSL settings searchable (#8881)
UtkarshUsername Aug 31, 2026
17f00f6
feat(web): add expand/collapse all control to the files surface (#8889)
UtkarshUsername Aug 31, 2026
0df043f
Add auto_review configuration to coderabbit.yaml
juliusmarminge Aug 31, 2026
85b656f
style: format CodeRabbit configuration
juliusmarminge Sep 1, 2026
9bc7a56
feat(mobile): upload attachments while composing (#8978)
juliusmarminge Sep 1, 2026
9ecfc07
fix(chat): keep agent activity visible between actions (#8984)
maria-rcks Sep 1, 2026
c78ae50
fix(server): isolate remote web session cookies (#8085)
Bil0000 Sep 1, 2026
42a8fd5
feat(pull-requests): link GitHub references in markdown (#8812)
maria-rcks Sep 1, 2026
b17cc3d
perf(server): reduce frequency of full tool call output being loaded …
t3dotgg Sep 1, 2026
d35c71d
feat(web): add pull request list filters (#8809)
maria-rcks Sep 1, 2026
ff93aba
feat(web): search individual settings by detail (#8831)
maria-rcks Sep 1, 2026
ce71c04
feat(client): render viewed images in work logs (#8936)
maria-rcks Sep 1, 2026
0947c30
fix(client): use package import for markdown image helpers (#9010)
juliusmarminge Sep 1, 2026
73776d4
test: remove static presentation snapshots (#9008)
t3-code[bot] Sep 1, 2026
a9ffb82
perf(server): bound snapshot activity payload memory (#9000)
t3dotgg Sep 1, 2026
0bfb6df
perf(server): cut idle CPU use and stop provider event leaks (#8187)
t3dotgg Sep 1, 2026
8f1ef8b
perf(server): scan only appended transcript bytes for usage summaries…
StiensWout Sep 1, 2026
7e4ce3b
perf(server): cut chatty tool-update frames by 90% (#8368)
Adamulek123 Sep 1, 2026
f32f9a2
fix(server): settle threads server-side (#8600)
t3dotgg Sep 1, 2026
8b033de
fix(clients): dedupe skills in composer menus (#8043)
Adamulek123 Sep 1, 2026
62d39bf
fix(server): stop OpenCode child sessions (#9005)
t3dotgg Sep 1, 2026
3c73fa7
perf(web): defer pull request line stats until visible (#6471)
Adamulek123 Sep 1, 2026
e86604d
perf(server): skip full-message reads while streaming (#9032)
t3dotgg Sep 1, 2026
b883fc0
perf(client-runtime): halve server config bootstrap traffic (#8367)
Adamulek123 Sep 1, 2026
10b5e79
merge: sync upstream/main through b883fc066 (41 commits)
github-actions[bot] Sep 1, 2026
02163fd
test(web): include fork catalog hits for settings search "work"
github-actions[bot] Sep 1, 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
4 changes: 4 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
reviews:
review_status: false
auto_review:
enabled: false
4 changes: 4 additions & 0 deletions .macroscope/approvability.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
Use Macroscope's default approvability criteria.

Additionally, any pull request that changes product defaults is not auto-approvable and requires human review.

Any pull request that adds or broadens a directive that disables or suppresses a lint,
type-checker, LSP, or other static-analysis diagnostic is not auto-approvable and requires
human review. This includes file-level, line-level, and configuration-level overrides.
1 change: 1 addition & 0 deletions .macroscope/check-run-agents/effect-service-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ Review changed TypeScript and directly affected call sites for the conventions b
## Change discipline

- Preserve useful comments, invariants, and specification documentation while moving code.
- Require every new or broadened directive that disables or suppresses a lint, type-checker, LSP, or other static-analysis diagnostic to have an adjacent comment explaining why that diagnostic must be disabled there. The directive itself is not an explanation. Report a missing explanation as a concrete violation.
- Do not add large tests solely to prove a mechanical refactor. Update existing tests and imports as needed.
- If backend behavior changes, require focused tests. Use test implementations/layers for external services only; do not mock out core business logic.
- Do not require `Layer.effect`, universal namespace imports, generic `make`/`layer` names for abstract-port implementations, separate error classes for diagnostic-only fields, or new tests for import-only changes.
Expand Down
3 changes: 0 additions & 3 deletions app.json

This file was deleted.

2 changes: 1 addition & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"@t3tools/ssh": "workspace:*",
"@t3tools/tailscale": "workspace:*",
"effect": "catalog:",
"electron": "41.5.0",
"electron": "43.4.1",
"electron-store": "^8.2.0",
"electron-updater": "^6.6.2",
"playwright-core": "1.60.0",
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/preview/BrowserSession.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ describe("BrowserSession", () => {
assert.strictEqual(browserSession.clearStorageData.mock.calls.length, 1);
assert.deepEqual(browserSession.clearStorageData.mock.calls[0], [
{
storages: ["cookies", "localstorage", "indexdb", "websql", "serviceworkers"],
storages: ["cookies", "localstorage", "indexdb", "serviceworkers"],
},
]);
assert.strictEqual(browserSession.clearCache.mock.calls.length, 1);
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/preview/BrowserSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export const make = Effect.gen(function* BrowserSessionMake() {
Effect.tryPromise({
try: () =>
browserSession.clearStorageData({
storages: ["cookies", "localstorage", "indexdb", "websql", "serviceworkers"],
storages: ["cookies", "localstorage", "indexdb", "serviceworkers"],
}),
catch: (cause) =>
new BrowserSessionStorageClearError({
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 @@ -53,8 +53,6 @@ const clientSettings: ClientSettings = {
planModeEnabled: false,
showSkillsInSlashMenu: false,
providerModelPreferences: {},
sidebarAutoSettleAfterDays: 3,
sidebarAutoSettleOnMerge: true,
sidebarProjectGroupingMode: "repository_path",
sidebarProjectGroupingOverrides: {
"environment-1:/tmp/project-a": "separate",
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ const config: ExpoConfig = {
},
NSLocalNetworkUsageDescription:
"Allow T3 Code to connect to T3 Code servers on your local network or tailnet.",
NSPhotoLibraryAddUsageDescription: "Allow T3 Code to save images to your photo library.",
ITSAppUsesNonExemptEncryption: false,
// The App Store screenshot harness rotates the iPad interface from
// inside the app (CI denies osascript the Accessibility access that
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,57 @@ import Security
import UIKit

public final class T3NativeControlsModule: Module {
private let presentationSources = T3PresentationSources()
private var videoPresentation: T3NativeVideoPresentation?
private var filePresentation: T3NativeFilePresentation?

public func definition() -> ModuleDefinition {
Name("T3NativeControls")

AsyncFunction("presentVideo") { (url: URL, title: String, sourceIdentifier: String, identifier: String, promise: Promise) in
try self.presentVideo(
url: url,
title: title,
sourceIdentifier: sourceIdentifier,
identifier: identifier,
promise: promise
)
}.runOnQueue(.main)

AsyncFunction("dismissVideo") { (identifier: String) in
self.dismissVideo(identifier: identifier)
}.runOnQueue(.main)

AsyncFunction("presentFile") { (url: URL, title: String, sourceIdentifier: String, identifier: String, promise: Promise) in
try self.presentFile(url: url, title: title, sourceIdentifier: sourceIdentifier,
identifier: identifier, promise: promise)
}.runOnQueue(.main)

AsyncFunction("dismissFile") { (identifier: String) in
self.dismissFile(identifier: identifier)
}.runOnQueue(.main)

OnDestroy {
let presentation = self.videoPresentation
let file = self.filePresentation
DispatchQueue.main.async {
presentation?.dismiss()
file?.dismiss()
}
}

View(T3PresentationSourceView.self) {
ViewName("PresentationSource")
Prop("identifier") { (view: T3PresentationSourceView, identifier: String) in
view.sources = self.presentationSources
view.identifier = identifier
}
}

AsyncFunction("shareFileFromSource") { (url: URL, title: String, identifier: String, promise: Promise) in
try self.shareFile(url: url, title: title, sourceIdentifier: identifier, promise: promise)
}.runOnQueue(.main)

Function("getShowcasePairingUrl") {
let arguments = ProcessInfo.processInfo.arguments
guard
Expand Down Expand Up @@ -101,4 +149,65 @@ public final class T3NativeControlsModule: Module {
try? scene.write(toFile: readyPath, atomically: true, encoding: .utf8)
}
}

private func presentVideo(url: URL, title: String, sourceIdentifier: String, identifier: String, promise: Promise) throws {
let isPlayableURL = url.isFileURL
? FileManager.default.isReadableFile(atPath: url.path)
: (["https", "http"].contains(url.scheme?.lowercased() ?? "") && url.host != nil)
guard videoPresentation == nil, filePresentation == nil,
let presenter = appContext?.utilities?.currentViewController(),
isPlayableURL
else {
throw NSError(
domain: "T3NativeVideo",
code: 2,
userInfo: [NSLocalizedDescriptionKey: "The video preview is no longer available."]
)
}
let presentation = T3NativeVideoPresentation(identifier: identifier, url: url, title: title) { [weak self] error in
self?.videoPresentation = nil
if let error { promise.reject(error) } else { promise.resolve(nil) }
}
videoPresentation = presentation
presentation.present(from: presenter, sources: presentationSources, sourceIdentifier: sourceIdentifier)
}

private func dismissVideo(identifier: String) {
if videoPresentation?.identifier == identifier { videoPresentation?.dismiss() }
}

private func presentFile(url: URL, title: String, sourceIdentifier: String,
identifier: String, promise: Promise) throws {
guard filePresentation == nil, videoPresentation == nil,
let presenter = appContext?.utilities?.currentViewController()
else { throw URLError(.cannotLoadFromNetwork) }
let file = T3NativeFilePresentation(identifier: identifier, sources: presentationSources,
sourceIdentifier: sourceIdentifier) { [weak self] error in
self?.filePresentation = nil
if let error { promise.reject(error) } else { promise.resolve(nil) }
}
filePresentation = file
file.present(url: url, title: title, from: presenter)
}

private func dismissFile(identifier: String) {
if filePresentation?.identifier == identifier { filePresentation?.dismiss() }
}

private func shareFile(url: URL, title: String, sourceIdentifier: String, promise: Promise) throws {
guard let presenter = appContext?.utilities?.currentViewController() else {
throw NSError(
domain: "T3NativePresentation",
code: 2,
userInfo: [NSLocalizedDescriptionKey: "The presenting screen is no longer open."]
)
}
try presentFileShare(
url: url,
title: title,
source: presentationSources.view(for: sourceIdentifier),
presenter: presenter,
promise: promise
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
import ImageIO
import QuickLook
import UIKit
import UniformTypeIdentifiers

private final class FilePreviewItem: NSObject, QLPreviewItem {
var previewItemURL: URL?
var previewItemTitle: String?
}

private final class FilePreviewController: QLPreviewController {
var onAppear: (() -> Void)?

override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
onAppear?()
}
}

/// Quick Look owns image and document controls, zooming, and source-view transitions.
final class T3NativeFilePresentation: NSObject, QLPreviewControllerDataSource,
QLPreviewControllerDelegate, UIAdaptivePresentationControllerDelegate {
let identifier: String
private var controller: UIViewController?
private let completion: (Error?) -> Void
private weak var sources: T3PresentationSources?
private let sourceIdentifier: String
private let item = FilePreviewItem()
private var loading: Task<Void, Never>?
private var dismissRequested = false
private var finished = false

init(identifier: String, sources: T3PresentationSources, sourceIdentifier: String, completion: @escaping (Error?) -> Void) {
self.identifier = identifier
self.sources = sources
self.sourceIdentifier = sourceIdentifier
self.completion = completion
super.init()
}

func present(url: URL, title: String, from presenter: UIViewController) {
loading = Task { @MainActor [self] in
do {
let file = try await Self.prepareFile(url: url, title: title)
guard !finished, !Task.isCancelled else {
try? FileManager.default.removeItem(at: file.deletingLastPathComponent())
return
}
item.previewItemURL = file
item.previewItemTitle = title
let preview = FilePreviewController()
preview.delegate = self
preview.dataSource = self
preview.onAppear = { [weak self] in self?.resumePendingDismissal() }
controller = preview
presenter.present(preview, animated: !UIAccessibility.isReduceMotionEnabled) { [self] in
resumePendingDismissal()
}
preview.presentationController?.delegate = self
} catch {
finish(error: error)
}
}
}

func dismiss() {
dismissRequested = true
loading?.cancel()
guard !finished else { return }
guard let controller else { finish(); return }
// Drain Close from viewDidAppear after opening or cancelling an interactive dismissal.
// Starting a second modal transition while UIKit is settling the first can strand it.
guard !controller.isBeingPresented, !controller.isBeingDismissed else { return }
controller.dismiss(animated: !UIAccessibility.isReduceMotionEnabled) { [self] in finish() }
}

private func resumePendingDismissal() {
// Appearance callbacks run before UIKit has cleared the current transition.
DispatchQueue.main.async { [weak self] in
if self?.dismissRequested == true { self?.dismiss() }
}
}

func numberOfPreviewItems(in controller: QLPreviewController) -> Int { item.previewItemURL == nil ? 0 : 1 }

func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
item
}

func previewController(_ controller: QLPreviewController, transitionViewFor item: QLPreviewItem) -> UIView? {
guard !UIAccessibility.isReduceMotionEnabled else { return nil }
return sources?.view(for: sourceIdentifier)
}

func previewController(_ controller: QLPreviewController, frameFor item: QLPreviewItem,
inSourceView view: AutoreleasingUnsafeMutablePointer<UIView?>) -> CGRect {
guard !UIAccessibility.isReduceMotionEnabled, let source = sources?.view(for: sourceIdentifier) else { return .zero }
view.pointee = source
return source.bounds
}

func previewControllerDidDismiss(_ controller: QLPreviewController) { finish() }

func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { finish() }

private func finish(error: Error? = nil) {
guard !finished else { return }
finished = true
loading?.cancel()
loading = nil
if let file = item.previewItemURL {
try? FileManager.default.removeItem(at: file.deletingLastPathComponent())
}
item.previewItemURL = nil
DispatchQueue.main.async { [completion] in completion(error) }
}

/// Copy original bytes so preview and sharing do not mutate a draft or workspace file.
nonisolated private static func prepareFile(url: URL, title: String) async throws -> URL {
try Task.checkCancellation()
let directory = FileManager.default.temporaryDirectory.appendingPathComponent("t3-preview-\(UUID().uuidString)")
try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
do {
let download = directory.appendingPathComponent("original")
if url.isFileURL {
try FileManager.default.copyItem(at: url, to: download)
} else if url.scheme == "data" {
try Data(contentsOf: url).write(to: download, options: .atomic)
} else {
guard ["https", "http"].contains(url.scheme?.lowercased() ?? "") else {
throw URLError(.unsupportedURL)
}
let (temporaryFile, response) = try await URLSession.shared.download(from: url)
guard let response = response as? HTTPURLResponse, (200..<300).contains(response.statusCode) else {
throw URLError(.badServerResponse)
}
try FileManager.default.moveItem(at: temporaryFile, to: download)
}
try Task.checkCancellation()
let type: UTType
if let image = CGImageSourceCreateWithURL(download as CFURL, nil),
CGImageSourceGetCount(image) > 0, let imageType = CGImageSourceGetType(image),
let detectedType = UTType(imageType as String) {
type = detectedType
} else if CGPDFDocument(download as CFURL) != nil {
type = .pdf
} else {
throw URLError(.cannotDecodeContentData)
}
let filename = URL(fileURLWithPath: title).lastPathComponent as NSString
let originalExtension = filename.pathExtension
let fileExtension = UTType(filenameExtension: originalExtension) == type
? originalExtension : type.preferredFilenameExtension ?? "png"
let stem = filename.deletingPathExtension
var name = String(stem.prefix(60)).components(separatedBy: .controlCharacters).joined(separator: "_")
while name.utf8.count > 200 { name.removeLast() }
let file = directory.appendingPathComponent("\(name.isEmpty ? "Preview" : name).\(fileExtension)")
try FileManager.default.moveItem(at: download, to: file)
return file
} catch {
try? FileManager.default.removeItem(at: directory)
throw error
}
}
}
Loading
Loading