Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
7cafe52
fix(server): keep thread titles tied to user intent (#10720)
t3dotgg Sep 15, 2026
e9b0555
Remove labels from effect service conventions
juliusmarminge Sep 15, 2026
537dc0f
Remove labels from ui-consistency.md
juliusmarminge Sep 15, 2026
970a873
Change conclusion status from failure to neutral
juliusmarminge Sep 15, 2026
8b9f6d3
Change conclusion from 'failure' to 'neutral'
juliusmarminge Sep 15, 2026
08abda9
refactor(server): resolve title links through source control provider…
juliusmarminge Sep 15, 2026
a62e7d6
refactor(server): align title generation with Effect conventions (#11…
juliusmarminge Sep 15, 2026
5623089
fix(server): disable color probes in worktree setup (#11843)
juliusmarminge Sep 15, 2026
0310cbf
fix: keep worktree setup visible after leaving and reopening the thre…
t3dotgg Sep 15, 2026
b20d29d
fix(desktop): prevent startup from running twice (#11857)
juliusmarminge Sep 15, 2026
26b8f98
feat(mobile): add iPad keyboard shortcuts and command palette (#11679)
bmdavis419 Sep 15, 2026
2c19283
feat(server): persist the worktree setup send and progress on the thr…
juliusmarminge Sep 15, 2026
cc839c4
feat(web): queue messages sent client-side while the agent is working…
t3dotgg Sep 15, 2026
5b377e2
fix(server): bound Git process bursts to keep connections responsive …
Bil0000 Sep 15, 2026
a37b852
perf(server): speed up worktree fetch and checkout (#11633)
Bil0000 Sep 15, 2026
9ea892e
fix(client): show thread state changes before remote replies (#11408)
Bil0000 Sep 15, 2026
6ecc15f
fix(mobile): restrict row highlighting to pointer input (#11863)
juliusmarminge Sep 15, 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: 1 addition & 4 deletions .macroscope/check-run-agents/effect-service-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,11 @@ include:
- "infra/**/*.ts"
exclude:
- "**/*.test.ts"
labels:
- vouch:trusted
- macroscope-review
requires:
- Check
maxBudgetPerRun: 5
maxBudgetPerPR: 25
conclusion: failure
conclusion: neutral
showToolCalls: true
---

Expand Down
5 changes: 1 addition & 4 deletions .macroscope/check-run-agents/ui-consistency.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,11 @@ include:
- "apps/web/src/**/*.css"
exclude:
- "apps/web/src/**/*.test.tsx"
labels:
- vouch:trusted
- macroscope-review
requires:
- Check
maxBudgetPerRun: 2
maxBudgetPerPR: 10
conclusion: failure
conclusion: neutral
---

# UI consistency review
Expand Down
110 changes: 110 additions & 0 deletions apps/desktop/scripts/main-process-bundle.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as NodeFSP from "node:fs/promises";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as NodeVM from "node:vm";
import { build } from "vite-plus/pack";
import { assert, it } from "vite-plus/test";

import desktopConfig from "../vite.config.ts";

it("keeps lazy Linux imports and worker bundles from executing desktop startup twice", async () => {
const directory = await NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "t3-desktop-bundle-"));
try {
const workerEntries = [
"src/electron/WindowsForegroundFocusWorker.ts",
"src/snapShot/GlobalShiftShortcutWorker.ts",
"src/snapShot/RegionSnapShotWorker.ts",
"src/snapShot/SnapShotAccessibilityWorker.ts",
];
await Promise.all([
NodeFSP.mkdir(NodePath.join(directory, "src/electron"), { recursive: true }),
NodeFSP.mkdir(NodePath.join(directory, "src/snapShot"), { recursive: true }),
]);
await Promise.all([
NodeFSP.writeFile(
NodePath.join(directory, "src/main.ts"),
`import { shared } from "./shared.ts";
process.emit("startup", shared.value);
void import("./linux.ts").then(({ result }) => process.emit("ready", result));`,
),
NodeFSP.writeFile(
NodePath.join(directory, "src/shared.ts"),
"export const shared = { value: 42 };",
),
NodeFSP.writeFile(
NodePath.join(directory, "src/linux.ts"),
'import { shared } from "./shared.ts"; export const result = shared.value + 1;',
),
...workerEntries.map((entry) =>
NodeFSP.writeFile(
NodePath.join(directory, entry),
'import { shared } from "../shared.ts"; process.emit("worker", shared.value);',
),
),
]);
assert.ok(Array.isArray(desktopConfig.pack));
const fixtureEntries = new Set(["src/main.ts", ...workerEntries]);
for (const packConfig of desktopConfig.pack) {
if (!Array.isArray(packConfig.entry)) continue;
if (!packConfig.entry.some((entry) => fixtureEntries.has(entry))) continue;
await build({
...packConfig,
config: false,
cwd: directory,
tsconfig: false,
sourcemap: false,
onSuccess: undefined,
logLevel: "silent",
});
}

const outputDirectory = NodePath.join(directory, "dist-electron");
const filenames = (await NodeFSP.readdir(outputDirectory, { recursive: true })).filter(
(filename) => filename.endsWith(".cjs"),
);
const sources = new Map(
await Promise.all(
filenames.map(async (filename) => {
const path = NodePath.join(outputDirectory, filename);
return [path, await NodeFSP.readFile(path, "utf8")];
}),
),
);
const modules = new Map();
const startups = [];
const workers = [];
const ready = Promise.withResolvers();
const load = (filename, cacheModule = true) => {
const cached = modules.get(filename);
if (cached) return cached.exports;
const module = { exports: {} };
if (cacheModule) modules.set(filename, module);
const source = sources.get(filename);
assert.ok(source, `Missing bundle: ${filename}`);
NodeVM.runInNewContext(source, {
exports: module.exports,
module,
require: (specifier) => load(NodePath.resolve(NodePath.dirname(filename), specifier)),
process: {
emit: (event, value) => {
if (event === "startup") startups.push(value);
if (event === "worker") workers.push(value);
if (event === "ready") ready.resolve(value);
},
},
});
return module.exports;
};

load(NodePath.join(outputDirectory, "main.cjs"), false);
assert.equal(await ready.promise, 43);
assert.deepEqual(startups, [42]);
for (const entry of workerEntries) {
load(NodePath.join(outputDirectory, entry.replace(/^src\//, "").replace(/\.ts$/, ".cjs")));
}
assert.deepEqual(workers, [42, 42, 42, 42]);
assert.deepEqual(startups, [42]);
} finally {
await NodeFSP.rm(directory, { recursive: true, force: true });
}
});
21 changes: 18 additions & 3 deletions apps/desktop/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,23 @@ export default defineConfig({
},
},
pack: [
{
format: "cjs",
outDir: "dist-electron",
dts: false,
sourcemap: true,
outExtensions: () => ({ js: ".cjs" }),
define: publicConfigDefine,
outputOptions: { codeSplitting: false },
entry: ["src/main.ts"],
clean: true,
deps: {
alwaysBundle: (id) => !id.startsWith("node:") && !isMainProcessExternal(id),
neverBundle: isMainProcessExternal,
onlyBundle: false,
},
...(shouldLaunchElectronAfterPack ? { onSuccess: "node scripts/dev-electron.mjs" } : {}),
},
{
format: "cjs",
outDir: "dist-electron",
Expand All @@ -56,19 +73,17 @@ export default defineConfig({
outExtensions: () => ({ js: ".cjs" }),
define: publicConfigDefine,
entry: [
"src/main.ts",
"src/electron/WindowsForegroundFocusWorker.ts",
"src/snapShot/GlobalShiftShortcutWorker.ts",
"src/snapShot/RegionSnapShotWorker.ts",
"src/snapShot/SnapShotAccessibilityWorker.ts",
],
clean: true,
clean: false,
deps: {
alwaysBundle: (id) => !id.startsWith("node:") && !isMainProcessExternal(id),
neverBundle: isMainProcessExternal,
onlyBundle: false,
},
...(shouldLaunchElectronAfterPack ? { onSuccess: "node scripts/dev-electron.mjs" } : {}),
},
{
format: "cjs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ public class T3ComposerEditorModule: Module {
Prop("spellCheck") { (view: T3ComposerEditorView, spellCheck: Bool) in
view.setSpellCheck(spellCheck)
}
Prop("enterBehavior") { (view: T3ComposerEditorView, behavior: String) in
view.setEnterBehavior(behavior)
}
Prop("textPasteThresholdBytes") { (view: T3ComposerEditorView, threshold: Int) in
view.setTextPasteThresholdBytes(threshold)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ private struct ComposerChipStyle {
let textColor: UIColor
}

private enum ComposerEnterBehavior: String {
case send
case newline
}

private final class ComposerTextAttachment: NSTextAttachment {
let source: String
let label: String
Expand Down Expand Up @@ -93,10 +98,12 @@ private final class ComposerTextView: UITextView {
var isReadOnly = false
var textPasteThresholdBytes = 0
var maxInputChars = Int.max
var enterBehavior: ComposerEnterBehavior = .send
private var bypassTextPasteInterception = false

override var keyCommands: [UIKeyCommand]? {
var commands = super.keyCommands ?? []
guard !isReadOnly, markedTextRange == nil else { return commands }
let submit = UIKeyCommand(
input: "\r",
modifierFlags: .command,
Expand All @@ -105,6 +112,25 @@ private final class ComposerTextView: UITextView {
submit.discoverabilityTitle = "Send Message"
submit.wantsPriorityOverSystemBehavior = true
commands.append(submit)
if enterBehavior == .send {
let submitOnReturn = UIKeyCommand(
input: "\r",
modifierFlags: [],
action: #selector(submitMessage(_:))
)
submitOnReturn.discoverabilityTitle = "Send Message"
submitOnReturn.wantsPriorityOverSystemBehavior = true
commands.append(submitOnReturn)

let newline = UIKeyCommand(
input: "\r",
modifierFlags: .shift,
action: #selector(insertNewline(_:))
)
newline.discoverabilityTitle = "New Line"
newline.wantsPriorityOverSystemBehavior = true
commands.append(newline)
}
if textPasteThresholdBytes > 0 {
let pasteAsText = UIKeyCommand(
input: "v",
Expand All @@ -119,9 +145,15 @@ private final class ComposerTextView: UITextView {
}

@objc private func submitMessage(_ sender: UIKeyCommand) {
guard !isReadOnly, markedTextRange == nil else { return }
onSubmit?()
}

@objc private func insertNewline(_ sender: UIKeyCommand) {
guard !isReadOnly, markedTextRange == nil else { return }
insertText("\n")
}

@objc private func pasteInline(_ sender: UIKeyCommand) {
guard !isReadOnly else {
return
Expand All @@ -132,6 +164,9 @@ private final class ComposerTextView: UITextView {
}

override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(submitMessage(_:)) || action == #selector(insertNewline(_:)) {
return isEditable && !isReadOnly && markedTextRange == nil
}
if isReadOnly && Self.readOnlyActions.contains(NSStringFromSelector(action)) {
return false
}
Expand Down Expand Up @@ -657,6 +692,10 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate, UITextDro
textView.spellCheckingType = spellCheck ? .yes : .no
}

func setEnterBehavior(_ behavior: String) {
textView.enterBehavior = ComposerEnterBehavior(rawValue: behavior) ?? .send
}

func setTextPasteThresholdBytes(_ threshold: Int) {
textView.textPasteThresholdBytes = threshold
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,26 @@ public final class T3KeyboardCommandsView: ExpoView {

public override var canBecomeFirstResponder: Bool { true }

public override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
if action == #selector(openCommandPalette) || action == #selector(paletteNext) || action == #selector(palettePrevious) || action == #selector(paletteDismiss),
let input = window?.t3FirstResponder as? UITextInput,
input.markedTextRange != nil {
return false
}
return super.canPerformAction(action, withSender: sender)
}

public override var keyCommands: [UIKeyCommand]? {
[
let isPad = UIDevice.current.userInterfaceIdiom == .pad
var commands = [
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"),
isPad
? enabledCommand("commandPalette", input: "k", modifiers: .command, action: #selector(openCommandPalette), title: "Command Palette")
: enabledCommand("focusSearch", input: "k", modifiers: .command, action: #selector(focusSearch), title: "Focus Search"),
enabledCommand("paletteNext", input: UIKeyCommand.inputDownArrow, modifiers: [], action: #selector(paletteNext), title: "Next Result"),
enabledCommand("palettePrevious", input: UIKeyCommand.inputUpArrow, modifiers: [], action: #selector(palettePrevious), title: "Previous Result"),
enabledCommand("paletteDismiss", input: UIKeyCommand.inputEscape, modifiers: [], action: #selector(paletteDismiss), title: "Close Command Palette"),
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"),
Expand All @@ -38,6 +53,18 @@ public final class T3KeyboardCommandsView: ExpoView {
),
enabledCommand("toggleSidebar", input: "\\", modifiers: .command, action: #selector(handleToggleSidebar), title: "Toggle Sidebar"),
].compactMap { $0 }
if isPad {
commands += (1...9).compactMap { index in
enabledCommand(
"thread.jump.\(index)",
input: String(index),
modifiers: .command,
action: #selector(jumpToThread(_:)),
title: "Go to Thread \(index)"
)
}
}
return commands
}

func setEnabledCommands(_ commands: [String]) {
Expand Down Expand Up @@ -108,6 +135,14 @@ public final class T3KeyboardCommandsView: ExpoView {
}

@objc private func newTask() { emit("newTask") }
@objc private func openCommandPalette() { emit("commandPalette") }
@objc private func paletteNext() { emit("paletteNext") }
@objc private func palettePrevious() { emit("palettePrevious") }
@objc private func paletteDismiss() { emit("paletteDismiss") }
@objc private func jumpToThread(_ sender: UIKeyCommand) {
guard let input = sender.input else { return }
emit("thread.jump.\(input)")
}
@objc private func focusSearch() { emit("focusSearch") }
@objc private func goBack() { emit("back") }
@objc private func openFiles() { emit("files") }
Expand Down
2 changes: 2 additions & 0 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ void SplashScreen.preventAutoHideAsync().catch(() => {

const appLinking = {
prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"],
// Keep the compact thread list available beneath a directly opened thread.
config: { initialRouteName: "Home" },
// The Expo dev client launches the app via
// <scheme>://expo-development-client/?url=<packager> — that URL addresses
// the launcher, not app navigation. Without this filter it falls through
Expand Down
Loading
Loading