Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
80d2b26
Add split-view mobile workspace layout
juliusmarminge Jun 23, 2026
cf0f731
Add split-view mobile navigation and native controls
juliusmarminge Jun 24, 2026
a896942
Improve mobile thread navigation and diff selection
juliusmarminge Jun 24, 2026
d353476
Memoize thread navigation callbacks
juliusmarminge Jun 24, 2026
8351e7c
Refine mobile layout for iPad and native glass headers
juliusmarminge Jun 26, 2026
babbf90
Update Expo iOS tooling and native glass header
Jun 27, 2026
32e1983
Bump Clerk and Expo dependencies
juliusmarminge Jun 27, 2026
4689c43
Refactor session handling and stream event processing
Jun 27, 2026
c4e63e0
Fix EAS iOS CocoaPods UUID cache
Jun 27, 2026
7f81813
Polish iPad mobile layout and native headers
Jun 27, 2026
ea8339e
Address iPad layout review feedback
Jun 27, 2026
75f0e90
Fix iPad header and input review issues
Jun 27, 2026
e4ec3cf
Resolve remaining iPad review issues
Jun 27, 2026
5efa2df
Address mobile review lifecycle issues
Jun 27, 2026
42a2275
Fix split inspector review followups
Jun 27, 2026
bcf070a
Fix file inspector route regressions
Jun 27, 2026
43b9724
Harden split inspector edge cases
Jun 27, 2026
bf00c7e
Fix sidebar selection and file return
Jun 27, 2026
c2aba43
Polish inspector routing behavior
Jun 27, 2026
80208bc
Stabilize inspector pane state
Jun 27, 2026
b83af8a
Reveal files inspector once
Jun 27, 2026
873a000
Adopt native iOS glass navigation chrome
Jun 29, 2026
618506f
Group native iOS header controls
Jun 29, 2026
5a1138c
Polish native phone chrome
Jun 29, 2026
ce72ed4
Restore native iPad thread search
Jun 29, 2026
a4472d0
Wire native iPad thread toolbar
Jun 29, 2026
59af9dc
Recover stale mobile thread routes
Jun 30, 2026
b5ef318
Use native split thread header title
Jun 30, 2026
fcb6b03
Align iPad detail header with Mail
Jun 30, 2026
89139e9
Stabilize iPad thread header controls
Jun 30, 2026
2f4fdfa
Align iPad thread controls with Mail
Jun 30, 2026
6c96dc7
Nudge iPad detail header controls inward
Jun 30, 2026
29ffec8
Hide split detail native title
Jun 30, 2026
036e481
Fix native iOS thread header integration
Jun 30, 2026
fdac1fb
Use Expo Router native home toolbar
Jun 30, 2026
051160c
Simplify iPhone thread native header
Jun 30, 2026
9e52f11
Use native glass header for terminal route
Jun 30, 2026
2c00d79
Use shared native scroll edge effect in thread surfaces
Jun 30, 2026
0f3201e
Use native edge chrome on file and review routes
Jun 30, 2026
3e4b357
Add native sidebar status subtitle
Jun 30, 2026
a0e0b6e
Add split home compose toolbar button
Jun 30, 2026
f5d4d7b
Stop forcing native glass debug route
Jun 30, 2026
b00deef
Match Mail sidebar selection on iPad
Jun 30, 2026
9083c71
Hide thread search while inspector owns search
Jun 30, 2026
af87cec
Add split sidebar control to terminal header
Jun 30, 2026
2caf6dd
Use Mail-style overflow controls on mobile
Jun 30, 2026
27606a9
Use native iPad archive header search
Jun 30, 2026
34379b6
Share archive row swipe behavior
Jun 30, 2026
1b7051a
Use native header search for file inspector
Jun 30, 2026
c11d0c6
Use native header for git inspector
Jun 30, 2026
c1f87cc
Show native search in iPad thread header
Jun 30, 2026
035eb3b
Centralize native Mail search toolbar config
Jun 30, 2026
7cddaba
Tune selected sidebar status pill contrast
Jun 30, 2026
ffdacaf
Use native Mail toolbar for file search
Jun 30, 2026
c70e6b5
Align inspector toolbar placement on iPad
Jun 30, 2026
8fb62e1
Use native glass header for file previews
Jun 30, 2026
dfbfc25
Centralize native header scroll edge effects
Jun 30, 2026
c77e6af
Use native scroll edge effects on list headers
Jun 30, 2026
92404ec
Use native new task sheet controls
Jun 30, 2026
a838270
Use compact native search in iPad thread header
Jun 30, 2026
258a36a
Flatten mobile not found link style
Jun 30, 2026
5bfd216
Remove stale thread toolbar search slot
Jun 30, 2026
a930a44
Polish native mobile Mail-style chrome
Jun 30, 2026
3158e1d
Group iPad detail header controls
Jun 30, 2026
856fc61
Tune native mobile glass toolbar grouping
Jun 30, 2026
c656e07
Wire iPad header search to thread sidebar
Jun 30, 2026
a423e3c
Move thread detail chrome to direct RNS
Jun 30, 2026
f6df16a
Replace Expo Router with native stack navigation
Jun 30, 2026
ee203ed
Structure mobile navigation around native stack
Jun 30, 2026
10ca14a
Tighten mobile navigation targets
Jul 1, 2026
7754433
Improve mobile settings navigation and cached connection state
Jul 8, 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
5 changes: 5 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"ios": {
"bundleIdentifier": "com.juliusmarminge.-t3toolsmonorepo"
}
}
1 change: 1 addition & 0 deletions apps/mobile/.swiftlint.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
included:
- ios/T3Code
- modules/t3-composer-editor/ios
- modules/t3-native-controls/ios
- modules/t3-terminal/ios
- modules/t3-review-diff/ios

Expand Down
11 changes: 10 additions & 1 deletion apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const VARIANT_CONFIG: Record<
readonly iosIcon: string;
readonly iosBundleIdentifier: string;
readonly androidPackage: string;
readonly relyingParty?: string;
}
> = {
development: {
Expand All @@ -25,20 +26,23 @@ const VARIANT_CONFIG: Record<
iosIcon: "./assets/icon-composer-dev.icon",
iosBundleIdentifier: "com.t3tools.t3code.dev",
androidPackage: "com.t3tools.t3code.dev",
relyingParty: "clerk.t3.codes",
},
preview: {
appName: "T3 Code Preview",
scheme: "t3code-preview",
iosIcon: "./assets/icon-composer-prod.icon",
iosBundleIdentifier: "com.t3tools.t3code.preview",
androidPackage: "com.t3tools.t3code.preview",
relyingParty: "clerk.t3.codes",
},
production: {
appName: "T3 Code",
scheme: "t3code",
iosIcon: "./assets/icon-composer-prod.icon",
iosBundleIdentifier: "com.t3tools.t3code",
androidPackage: "com.t3tools.t3code",
relyingParty: "clerk.t3.codes",
},
};

Expand Down Expand Up @@ -77,6 +81,10 @@ const config: ExpoConfig = {
icon: variant.iosIcon,
supportsTablet: true,
bundleIdentifier: variant.iosBundleIdentifier,
associatedDomains: [
`applinks:${variant.relyingParty}`,
`webcredentials:${variant.relyingParty}`,
],
infoPlist: {
NSAppTransportSecurity: {
NSAllowsArbitraryLoads: true,
Expand All @@ -101,7 +109,6 @@ const config: ExpoConfig = {
favicon: "./assets/favicon.png",
},
plugins: [
"expo-router",
"expo-font",
"expo-secure-store",
["@clerk/expo", { theme: "./clerk-theme.json" }],
Expand Down Expand Up @@ -139,6 +146,7 @@ const config: ExpoConfig = {
},
},
],
"./plugins/withIosCocoaPodsUuidCache.cjs",
[
"expo-widgets",
{
Expand All @@ -155,6 +163,7 @@ const config: ExpoConfig = {
],
},
],
"./plugins/withIosSceneLifecycle.cjs",
"./plugins/withAndroidCleartextTraffic.cjs",
],
extra: {
Expand Down
6 changes: 5 additions & 1 deletion apps/mobile/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
import "react-native-gesture-handler";
import "expo-router/entry";
import { registerRootComponent } from "expo";

import App from "./src/App";

registerRootComponent(App);
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public class T3ComposerEditorModule: Module {
"onComposerSelectionChange",
"onComposerFocus",
"onComposerBlur",
"onComposerSubmit",
"onComposerPasteImages",
"onComposerContentSizeChange"
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,24 @@ private final class ComposerTextView: UITextView {

var onPasteImages: (([String]) -> Void)?
var onAttributedMutation: (() -> Void)?
var onSubmit: (() -> Void)?

override var keyCommands: [UIKeyCommand]? {
var commands = super.keyCommands ?? []
let submit = UIKeyCommand(
input: "\r",
modifierFlags: .command,
action: #selector(submitMessage(_:))
)
submit.discoverabilityTitle = "Send Message"
submit.wantsPriorityOverSystemBehavior = true
commands.append(submit)
return commands
}

@objc private func submitMessage(_ sender: UIKeyCommand) {
onSubmit?()
}

override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(paste(_:)) {
Expand Down Expand Up @@ -299,6 +317,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate {
let onComposerSelectionChange = EventDispatcher()
let onComposerFocus = EventDispatcher()
let onComposerBlur = EventDispatcher()
let onComposerSubmit = EventDispatcher()
let onComposerPasteImages = EventDispatcher()
let onComposerContentSizeChange = EventDispatcher()

Expand All @@ -320,6 +339,9 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate {
textView.onAttributedMutation = { [weak self] in
self?.emitTextChange()
}
textView.onSubmit = { [weak self] in
self?.onComposerSubmit([:])
}
addSubview(textView)

placeholderLabel.numberOfLines = 0
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"platforms": ["apple"],
"apple": {
"modules": ["T3NativeControlsModule", "T3KeyboardCommandsModule"]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import ExpoModulesCore
import UIKit

public final class T3HeaderButtonView: ExpoView {
private static let size: CGFloat = 44
private static let symbolSize: CGFloat = 18

private let button = UIButton(type: .system)
private var systemImage = "circle"

let onTriggered = EventDispatcher()

public required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)

isAccessibilityElement = false
button.frame = bounds
button.autoresizingMask = [.flexibleWidth, .flexibleHeight]
button.addTarget(self, action: #selector(handlePress), for: .primaryActionTriggered)
addSubview(button)
applyConfiguration()
}

public override var intrinsicContentSize: CGSize {
CGSize(width: Self.size, height: Self.size)
}

public func setLabel(_ label: String) {
button.accessibilityLabel = label
}

public func setSystemImage(_ systemImage: String) {
guard self.systemImage != systemImage else {
return
}
self.systemImage = systemImage
applyConfiguration()
}

private func applyConfiguration() {
var configuration: UIButton.Configuration
if #available(iOS 26.0, *) {
configuration = .glass()
configuration.cornerStyle = .capsule
} else {
configuration = .plain()
}

configuration.baseForegroundColor = .label
configuration.contentInsets = .zero
configuration.image = UIImage(systemName: systemImage)
configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration(
pointSize: Self.symbolSize,
weight: .regular
)
button.configuration = configuration
}

@objc private func handlePress() {
onTriggered()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import ExpoModulesCore
import UIKit

public final class T3KeyboardCommandsModule: Module {
public func definition() -> ModuleDefinition {
Name("T3KeyboardCommands")

View(T3KeyboardCommandsView.self) {
Prop("enabledCommands") { (view: T3KeyboardCommandsView, commands: [String]) in
view.setEnabledCommands(commands)
}
Events("onCommand")
}
}
}

public final class T3KeyboardCommandsView: ExpoView {
let onCommand = EventDispatcher()
private var enabledCommands = Set<String>()

public override var canBecomeFirstResponder: Bool { true }

public override var keyCommands: [UIKeyCommand]? {
[
enabledCommand("newTask", input: "n", modifiers: .command, action: #selector(newTask), title: "New Task"),
enabledCommand("focusSearch", input: "f", modifiers: .command, action: #selector(focusSearch), title: "Find"),
enabledCommand("focusSearch", input: "k", modifiers: .command, action: #selector(focusSearch), title: "Focus Search"),
enabledCommand("back", input: "[", modifiers: .command, action: #selector(goBack), title: "Back"),
enabledCommand("files", input: "f", modifiers: [.command, .shift], action: #selector(openFiles), title: "Open Files"),
enabledCommand("terminal", input: "t", modifiers: [.command, .shift], action: #selector(openTerminal), title: "Open Terminal"),
enabledCommand("review", input: "r", modifiers: [.command, .shift], action: #selector(openReview), title: "Open Review"),
enabledCommand("toggleSidebar", input: "\\", modifiers: .command, action: #selector(handleToggleSidebar), title: "Toggle Sidebar"),
].compactMap { $0 }
}

func setEnabledCommands(_ commands: [String]) {
enabledCommands = Set(commands)
if isFirstResponder {
resignFirstResponder()
}
reclaimFirstResponderIfAvailable()
}

private func enabledCommand(
_ identifier: String,
input: String,
modifiers: UIKeyModifierFlags,
action: Selector,
title: String
) -> UIKeyCommand? {
guard enabledCommands.contains(identifier) else { return nil }
return command(input, modifiers: modifiers, action: action, title: title)
}

public required init(appContext: AppContext? = nil) {
super.init(appContext: appContext)
NotificationCenter.default.addObserver(
self,
selector: #selector(reclaimFirstResponderIfAvailable),
name: UITextView.textDidEndEditingNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(reclaimFirstResponderIfAvailable),
name: UITextField.textDidEndEditingNotification,
object: nil
)
NotificationCenter.default.addObserver(
self,
selector: #selector(reclaimFirstResponderIfAvailable),
name: UIApplication.didBecomeActiveNotification,
object: nil
)
}

deinit {
NotificationCenter.default.removeObserver(self)
}

public override func didMoveToWindow() {
super.didMoveToWindow()
reclaimFirstResponderIfAvailable()
}

public override func layoutSubviews() {
super.layoutSubviews()
reclaimFirstResponderIfAvailable()
}

private func command(
_ input: String,
modifiers: UIKeyModifierFlags,
action: Selector,
title: String
) -> UIKeyCommand {
let command = UIKeyCommand(input: input, modifierFlags: modifiers, action: action)
command.discoverabilityTitle = title
command.wantsPriorityOverSystemBehavior = true
return command
}

@objc private func newTask() { emit("newTask") }
@objc private func focusSearch() { emit("focusSearch") }
@objc private func goBack() { emit("back") }
@objc private func openFiles() { emit("files") }
@objc private func openTerminal() { emit("terminal") }
@objc private func openReview() { emit("review") }
@objc private func handleToggleSidebar() { emit("toggleSidebar") }

private func emit(_ command: String) {
onCommand(["command": command])
}

@objc private func reclaimFirstResponderIfAvailable() {
DispatchQueue.main.async { [weak self] in
guard let self, self.window?.t3FirstResponder == nil else { return }
self.becomeFirstResponder()
}
}
}

private extension UIView {
var t3FirstResponder: UIResponder? {
if isFirstResponder { return self }
for subview in subviews {
if let responder = subview.t3FirstResponder { return responder }
}
return nil
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Pod::Spec.new do |s|
s.name = 'T3NativeControls'
s.version = '1.0.0'
s.summary = 'Native UIKit controls for T3 Code mobile.'
s.description = 'UIKit-backed controls that match native iOS navigation chrome.'
s.author = 'T3 Tools'
s.homepage = 'https://t3tools.com'
s.platforms = {
:ios => '18.0',
}
s.source = { :path => '.' }
s.static_framework = true

s.dependency 'ExpoModulesCore'
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
}
s.source_files = '**/*.{h,m,mm,swift,hpp,cpp}'
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import ExpoModulesCore

public final class T3NativeControlsModule: Module {
public func definition() -> ModuleDefinition {
Name("T3NativeControls")

View(T3HeaderButtonView.self) {
Prop("label") { (view: T3HeaderButtonView, label: String) in
view.setLabel(label)
}
Prop("systemImage") { (view: T3HeaderButtonView, systemImage: String) in
view.setSystemImage(systemImage)
}

Events("onTriggered")
}
}
}
Loading
Loading