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
25bf84a
fix(mobile): center the Choose project empty-state buttons (#27450)
juliusmarminge Nov 7, 2026
511238c
fix(mobile): Android terminal sends hardware-keyboard arrows, Tab, an…
only21mil Nov 7, 2026
0080084
fix(mobile): group settled PR threads in the continue menu (#27941)
arifali123 Nov 7, 2026
af96a01
fix(mobile): shorten the model picker save action (#32551)
PixPMusic Nov 7, 2026
a9e709f
fix(mobile): center Android header confirmation buttons (#32239)
PixPMusic Nov 7, 2026
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
1a0ab0c
chore(fork): merge upstream changes through 1d78b3320
gabynevada Nov 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
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
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,10 @@ class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(contex
onInput(mapOf("data" to "\r"))
},
)
val navigationKeys = TerminalNavigationKeyListener { onInput(mapOf("data" to it)) }
inputView.setOnKeyListener { _, keyCode, event ->
if (readOnly) return@setOnKeyListener true
if (navigationKeys.onKey(inputView, keyCode, event)) return@setOnKeyListener true
if (event.action != KeyEvent.ACTION_DOWN) return@setOnKeyListener false
when {
keyCode == KeyEvent.KEYCODE_DEL -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package expo.modules.t3terminal

import android.view.KeyEvent
import android.view.View

internal class TerminalNavigationKeyListener(
private val send: (String) -> Unit
) : View.OnKeyListener {
override fun onKey(view: View?, keyCode: Int, event: KeyEvent): Boolean {
val sequence = sequenceFor(keyCode, event) ?: return false
if (event.action == KeyEvent.ACTION_DOWN) send(sequence)
// Unconsumed arrow and Tab key events move focus off the terminal input.
return true
}

// Matches iOS: these keys only without modifiers, plus Shift+Tab.
private fun sequenceFor(keyCode: Int, event: KeyEvent): String? = when {
keyCode == KeyEvent.KEYCODE_TAB && event.hasModifiers(KeyEvent.META_SHIFT_ON) -> "\u001b[Z"
!event.hasNoModifiers() -> null
keyCode == KeyEvent.KEYCODE_DPAD_UP -> "\u001b[A"
keyCode == KeyEvent.KEYCODE_DPAD_DOWN -> "\u001b[B"
keyCode == KeyEvent.KEYCODE_DPAD_RIGHT -> "\u001b[C"
keyCode == KeyEvent.KEYCODE_DPAD_LEFT -> "\u001b[D"
keyCode == KeyEvent.KEYCODE_TAB -> "\t"
keyCode == KeyEvent.KEYCODE_ESCAPE -> "\u001b"
else -> null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package expo.modules.t3terminal

import android.app.Activity
import android.view.KeyEvent
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.LinearLayout
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [36], manifest = Config.NONE)
class TerminalNavigationKeyListenerTest {
@Test
fun hardwareNavigationKeysReachThePtyAndKeepFocus() {
val activity = Robolectric.buildActivity(Activity::class.java).setup().get()
val sent = StringBuilder()
val input = EditText(activity).apply {
setSingleLine(true)
isFocusableInTouchMode = true
setOnKeyListener(TerminalNavigationKeyListener { sent.append(it) })
}
val nextControl = Button(activity).apply { isFocusableInTouchMode = true }
val root = LinearLayout(activity).apply {
orientation = LinearLayout.VERTICAL
addView(input)
addView(nextControl)
}
activity.setContentView(root)
// Focus search below the input only finds controls that have bounds.
root.measure(
View.MeasureSpec.makeMeasureSpec(1080, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY),
)
root.layout(0, 0, 1080, 1920)
assertTrue(input.requestFocus())

val shift = KeyEvent.META_SHIFT_ON or KeyEvent.META_SHIFT_LEFT_ON
val keys = listOf(
Triple(KeyEvent.KEYCODE_DPAD_UP, 0, "\u001b[A"),
Triple(KeyEvent.KEYCODE_DPAD_DOWN, 0, "\u001b[B"),
Triple(KeyEvent.KEYCODE_DPAD_RIGHT, 0, "\u001b[C"),
Triple(KeyEvent.KEYCODE_DPAD_LEFT, 0, "\u001b[D"),
Triple(KeyEvent.KEYCODE_TAB, 0, "\t"),
Triple(KeyEvent.KEYCODE_TAB, shift, "\u001b[Z"),
Triple(KeyEvent.KEYCODE_ESCAPE, 0, "\u001b"),
)
for ((keyCode, meta, expected) in keys) {
sent.clear()
input.dispatchKeyEvent(KeyEvent(0, 0, KeyEvent.ACTION_DOWN, keyCode, 0, meta))
input.dispatchKeyEvent(KeyEvent(0, 0, KeyEvent.ACTION_UP, keyCode, 0, meta))
assertEquals("bytes for key $keyCode meta $meta", expected, sent.toString())
assertTrue("focus left the input after key $keyCode meta $meta", input.isFocused)
}

sent.clear()
val ctrl = KeyEvent.META_CTRL_ON or KeyEvent.META_CTRL_LEFT_ON
input.dispatchKeyEvent(KeyEvent(0, 0, KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP, 0, ctrl))
assertEquals("modified arrows are left to the app, as on iOS", "", sent.toString())
}
}
5 changes: 5 additions & 0 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ import {
import {
ScheduledTaskModelPickerRouteScreen,
ScheduledTaskBranchPickerRouteScreen,
ScheduledTaskThreadPickerRouteScreen,
} from "./features/settings/ScheduledTaskPickerScreens";
import { SettingsKeyboardRouteScreen } from "./features/settings/SettingsKeyboardRouteScreen";
import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen";
Expand Down Expand Up @@ -301,6 +302,10 @@ const SETTINGS_SCREENS = {
screen: ScheduledTaskBranchPickerRouteScreen,
options: settingsPage("Base branch"),
}),
SettingsScheduledTaskThread: createNativeStackScreen({
screen: ScheduledTaskThreadPickerRouteScreen,
options: settingsPage("Destination"),
}),
SettingsScheduledTaskModel: createNativeStackScreen({
screen: ScheduledTaskModelPickerRouteScreen,
options: THREAD_SETTINGS_ROUTE,
Expand Down
16 changes: 16 additions & 0 deletions apps/mobile/src/components/FilePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import ImageViewing from "react-native-image-viewing";
import { useSafeAreaInsets } from "react-native-safe-area-context";

import { openAttachmentInViewer } from "../lib/attachmentDownload";
import { useMediaActions } from "../state/mediaActions";
import type {
ResolvedFilePreviewDocument,
ResolvedFilePreviewSource,
Expand All @@ -20,6 +21,8 @@ function PdfDocumentPreview(props: {
readonly onRequestClose: () => void;
}) {
const insets = useSafeAreaInsets();
const media = useMediaActions(props.source.actionsSource);
const save = media.actions.find((action) => action.id === "save");
return (
<Modal
animationType="slide"
Expand All @@ -35,6 +38,19 @@ function PdfDocumentPreview(props: {
presentation="inline"
title={props.source.name ?? "PDF"}
onClose={props.onRequestClose}
actions={
save
? [
{
accessibilityLabel: "Save or share",
icon: "square.and.arrow.up",
disabled: save.disabled,
loading: media.sharing,
onPress: () => void save.run(),
},
]
: []
}
/>
<PdfPreview uri={props.source.uri} />
</View>
Expand Down
22 changes: 21 additions & 1 deletion apps/mobile/src/components/FilePreviewModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { refreshAssetUrl } from "../state/assets";
import { filePreviewGallery, filePreviewKey, filePreviewName } from "../lib/filePreviewGallery";
import { FilePreviewGallery } from "./FilePreviewGallery";
import { FilePreview } from "./FilePreview";
import type { MediaActionsSource } from "../lib/mediaActionsSource";
import type { FilePreviewItem, FilePreviewSource } from "./FilePreviewModal.types";

export type {
Expand Down Expand Up @@ -67,6 +68,18 @@ async function resolvePreviewUri(
return { uri: url + (item.srcFragment ?? "") };
}

// Shares from the unresolved item so remote URLs refresh and draft files take their own lease.
function pdfActionsSource(item: FilePreviewItem): MediaActionsSource {
const metadata = {
name: filePreviewName(item),
mimeType: item.mimeType ?? "application/pdf",
...(item.sourceIdentifier ? { sourceIdentifier: item.sourceIdentifier } : {}),
};
if ("uri" in item) return { ...metadata, uri: item.uri };
if ("attachment" in item) return { ...metadata, attachment: item.attachment };
return { ...metadata, environmentId: item.environmentId, resource: item.resource };
}

function ResolvedFilePreview(props: {
readonly source: FilePreviewSource;
readonly onRequestClose: () => void;
Expand Down Expand Up @@ -166,9 +179,16 @@ function ResolvedFilePreview(props: {
: []),
]);
};
const actionsSource =
selected.kind === "pdf" ? (selected.actionsSource ?? pdfActionsSource(selected)) : undefined;
return (
<FilePreview
source={{ ...selected, kind: selected.kind, uri }}
source={{
...selected,
kind: selected.kind,
uri,
...(actionsSource ? { actionsSource } : {}),
}}
onRequestClose={props.onRequestClose}
onOpenError={onOpenError}
/>
Expand Down
16 changes: 9 additions & 7 deletions apps/mobile/src/components/ui/AppBar.android.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,13 +155,15 @@ function AppBarView(props: AppBarProps & { readonly searchState?: SearchState })
</Menu>
));
const confirmButton = props.confirm ? (
<Button
label={props.confirm.label}
tone="text"
loading={props.confirm.loading}
disabled={props.confirm.disabled}
onPress={props.confirm.onPress}
/>
<View className="self-center">
<Button
label={props.confirm.label}
tone="text"
loading={props.confirm.loading}
disabled={props.confirm.disabled}
onPress={props.confirm.onPress}
/>
</View>
) : null;
const options =
props.embedded || props.sheet ? null : (
Expand Down
10 changes: 10 additions & 0 deletions apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,16 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) {
kind: "pdf",
uri: previewUri,
name: basename(relativePath),
...(environmentId !== null && assetPreview.resource !== null
? {
actionsSource: {
name: basename(relativePath),
mimeType: "application/pdf",
environmentId,
resource: assetPreview.resource,
},
}
: {}),
}),
} as const)
: null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,13 @@ export function PullRequestAgentRow(props: {
title={props.title ?? action.title}
icon="sparkles"
accessibilityLabel={props.title ?? action.title}
menu={agent.threads.length > 1}
menu={agent.chooseThread && !action.attributes.disabled}
onPress={
action.attributes.disabled || agent.threads.length > 1
? undefined
: () => agent.start(props.task)
action.attributes.disabled || agent.chooseThread ? undefined : () => agent.start(props.task)
}
/>
);
if (agent.threads.length <= 1 || action.attributes.disabled) return row;
if (!agent.chooseThread || action.attributes.disabled) return row;
return (
<Menu
title={props.title ?? action.title}
Expand Down
Loading
Loading