Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
0e0ddae
feat(web): add opt-in thread notifications and sounds (#11481)
maria-rcks Sep 13, 2026
c299764
fix(server): open Cursor links in classic IDE mode (#11498)
Yash-Singh1 Sep 13, 2026
6fd68f5
feat(source-control): support Forgejo and Gitea with fj and tea (#11436)
maria-rcks Sep 13, 2026
0c5771d
fix(web): match draft row heights to thread rows (#11512)
Yash-Singh1 Sep 13, 2026
3138f57
fix(grok): emit task lifecycle for monitors and background shells (#9…
Svyk Sep 13, 2026
46140c9
fix(web): unify panel resizing and retain final drag width (#11529)
maria-rcks Sep 13, 2026
2ec59ca
fix(web): hide back button for single linked pull requests (#11520)
maria-rcks Sep 13, 2026
21d53ca
fix(files): browse ignored files and load folders on demand (#11527)
maria-rcks Sep 13, 2026
d7c71f9
feat(web): float the pull request comment composer (#11531)
maria-rcks Sep 13, 2026
8b3ddf5
fix(mobile): stop crashing on launch before the shell snapshot arrive…
juliusmarminge Sep 13, 2026
db6e053
feat(github): route pull request operations across matching accounts …
maria-rcks Sep 13, 2026
20a8f1d
chore(mobile): enable noUncheckedIndexedAccess and noImplicitOverride…
juliusmarminge Sep 13, 2026
0a91b9a
feat(mobile): show startup crashes in Settings → Diagnostics (#11540)
juliusmarminge Sep 13, 2026
17f8e2a
feat(mobile): add pooled subscription usage widgets (#11506)
MatthewFeroz Sep 13, 2026
20363c3
feat(web): add provider selector to pull request toolbar (#11524)
maria-rcks Sep 13, 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
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as DesktopClientSettings from "./DesktopClientSettings.ts";

const clientSettings: ClientSettings = {
...DEFAULT_CLIENT_SETTINGS,
notificationMode: "notifications-and-sound",
appearanceContrast: 100,
browserDefaultViewport: { _tag: "preset", width: 1024, height: 600, presetId: "nest-hub" },
browserDefaultZoomFactor: 1.25,
Expand Down
39 changes: 39 additions & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,45 @@ const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [
// frequent-updates entitlement iOS throttles the update budget sooner.
frequentUpdates: true,
widgets: [
{
name: "SubscriptionUsage",
displayName: "Subscription usage",
description: "Subscription quotas from your connected T3 Code environments.",
configuration: {
title: "Subscription usage",
description:
"Both shows Session and Weekly when available. The Lock Screen shows the tightest selected limit.",
parameters: {
codexPeriod: {
title: "Codex limits",
type: "enum",
default: "auto",
values: [
{ name: "Both", value: "auto" },
{ name: "Session", value: "session" },
{ name: "Weekly", value: "weekly" },
],
},
claudePeriod: {
title: "Claude limits",
type: "enum",
default: "auto",
values: [
{ name: "Both", value: "auto" },
{ name: "Session", value: "session" },
{ name: "Weekly", value: "weekly" },
],
},
},
},
supportedFamilies: [
"systemSmall",
"systemMedium",
"systemLarge",
"systemExtraLarge",
"accessoryRectangular",
],
},
{
name: "AgentActivity",
displayName: "Agent Activity",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function resolveHeadingFontSize(textStyle: NativeMarkdownTextStyle, headingLevel
}

const scale = textStyle.fontSize / DEFAULT_BODY_FONT_SIZE;
return Math.max(12, Math.round(DEFAULT_HEADING_FONT_SIZES[index] * scale));
return Math.max(12, Math.round((DEFAULT_HEADING_FONT_SIZES[index] ?? 15) * scale));
}

function runStyle(run: NativeMarkdownTextRun, textStyle: NativeMarkdownTextStyle): TextStyle {
Expand Down
18 changes: 18 additions & 0 deletions apps/mobile/modules/t3-subscription-widget/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
apply plugin: 'com.android.library'
apply plugin: 'org.jetbrains.kotlin.android'

group = 'com.t3tools.subscriptionwidget'
version = '0.0.0'

android {
namespace 'expo.modules.t3subscriptionwidget'
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
}
}

dependencies {
implementation project(':expo-modules-core')
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<receiver android:name="expo.modules.t3subscriptionwidget.SubscriptionUsageWidget" android:exported="false" android:label="@string/t3_subscription_widget_name">
<intent-filter><action android:name="android.appwidget.action.APPWIDGET_UPDATE" /></intent-filter>
<meta-data android:name="android.appwidget.provider" android:resource="@xml/t3_subscription_widget_info" />
</receiver>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package expo.modules.t3subscriptionwidget

import android.app.AlarmManager
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.widget.RemoteViews
import org.json.JSONObject
import java.text.DateFormat
import java.util.Date

class SubscriptionUsageWidget : AppWidgetProvider() {
override fun onUpdate(context: Context, manager: AppWidgetManager, ids: IntArray) {
ids.forEach { update(context, manager, it) }
}

override fun onReceive(context: Context, intent: Intent) {
super.onReceive(context, intent)
if (intent.action == EXPIRE) updateAll(context)
}

override fun onDisabled(context: Context) {
context.getSystemService(AlarmManager::class.java).cancel(expiryIntent(context))
}

override fun onAppWidgetOptionsChanged(
context: Context,
manager: AppWidgetManager,
id: Int,
options: Bundle
) {
update(context, manager, id)
}

companion object {
const val PREFERENCES = "t3_subscription_widget"
private const val EXPIRE = "expo.modules.t3subscriptionwidget.EXPIRE"

private fun expiryIntent(context: Context): PendingIntent = PendingIntent.getBroadcast(
context,
0,
Intent(context, SubscriptionUsageWidget::class.java).setAction(EXPIRE),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

fun updateAll(context: Context) {
val manager = AppWidgetManager.getInstance(context)
manager.getAppWidgetIds(ComponentName(context, SubscriptionUsageWidget::class.java))
.forEach { update(context, manager, it) }
}

private fun update(context: Context, manager: AppWidgetManager, id: Int) {
val saved = context.getSharedPreferences(PREFERENCES, 0).getString("snapshot", null)
val snapshot = runCatching { JSONObject(saved.orEmpty()) }.getOrNull()
val views = RemoteViews(context.packageName, R.layout.t3_subscription_widget)
openAppIntent(context, id, snapshot)?.let {
views.setOnClickPendingIntent(R.id.t3_widget_root, it)
}
val providers = snapshot?.optJSONArray("providers")
val now = System.currentTimeMillis()
var nextExpiry = Long.MAX_VALUE
var totalRows = 0
val groups = (0 until (providers?.length() ?: 0)).mapNotNull { index ->
val provider = providers?.optJSONObject(index) ?: return@mapNotNull null
val windows = provider.optJSONArray("windows")
val expiresAt = provider.optLong("expiresAt")
if (expiresAt > now && windows != null && windows.length() > 0) {
nextExpiry = minOf(nextExpiry, expiresAt)
totalRows += provider.optInt("totalWindows", windows.length())
(0 until windows.length()).map { provider to windows.optJSONObject(it) }
} else {
totalRows++
listOf(provider to null)
}
}
// Show each provider before filling spare space with its other windows.
val rows = (0 until (groups.maxOfOrNull { it.size } ?: 0)).flatMap { index ->
groups.mapNotNull { it.getOrNull(index) }
}
if (rows.isNotEmpty()) {
views.removeAllViews(R.id.t3_widget_rows)
val options = manager.getAppWidgetOptions(id)
val height = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 180)
val count = ((height - 64) / 66).coerceIn(1, 12).coerceAtMost(rows.size)
for ((provider, window) in rows.take(count)) {
views.addView(R.id.t3_widget_rows, rowView(context, provider, window))
}
val remaining = totalRows - count
val checkedAt = snapshot?.optLong("checkedAt") ?: 0
val formatted = DateFormat.getDateTimeInstance(
DateFormat.SHORT,
DateFormat.SHORT
).format(Date(checkedAt))
val more = if (remaining > 0) {
context.getString(R.string.t3_subscription_widget_more, remaining)
} else {
""
}
val checked = if (checkedAt > 0) {
context.getString(R.string.t3_subscription_widget_as_of, formatted)
} else {
context.getString(R.string.t3_subscription_widget_unknown_check)
}
views.setTextViewText(R.id.t3_widget_footer, checked + more)
}
val alarms = context.getSystemService(AlarmManager::class.java)
alarms.cancel(expiryIntent(context))
// Inexact and non-wakeup: the timestamp remains visible if Android delays expiry.
if (nextExpiry != Long.MAX_VALUE) {
alarms.set(AlarmManager.RTC, nextExpiry, expiryIntent(context))
}
manager.updateAppWidget(id, views)
}

private fun openAppIntent(context: Context, id: Int, snapshot: JSONObject?): PendingIntent? {
// Target this variant's launcher so co-installed builds cannot steal the tap.
val intent =
context.packageManager.getLaunchIntentForPackage(context.packageName) ?: return null
intent.action = Intent.ACTION_VIEW
val deepLink = snapshot?.optString("url")?.takeIf { it.isNotBlank() }
?: "t3code://settings/usage?tab=limits"
intent.data = Uri.parse(deepLink)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
return PendingIntent.getActivity(
context,
id,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}

private fun rowView(context: Context, provider: JSONObject, window: JSONObject?): RemoteViews {
val child = RemoteViews(context.packageName, R.layout.t3_subscription_widget_row)
val remaining = window?.optInt("remaining")?.coerceIn(0, 100)
val detail = provider.optString("detail")
val label = provider.optString("name")
val windowLabel = window?.optString("label") ?: detail
child.setTextViewText(R.id.t3_widget_label, label)
child.setTextViewText(R.id.t3_widget_window, windowLabel)
val percent = remaining?.let {
context.getString(R.string.t3_subscription_widget_remaining, it)
} ?: "—"
child.setTextViewText(R.id.t3_widget_percent, percent)
val visibility = if (remaining == null) View.GONE else View.VISIBLE
child.setViewVisibility(R.id.t3_widget_progress, visibility)
if (remaining != null) child.setProgressBar(R.id.t3_widget_progress, 100, remaining, false)
val reset = window?.optString("reset")
?: context.getString(R.string.t3_subscription_widget_refresh)
child.setTextViewText(R.id.t3_widget_reset, reset)
child.setContentDescription(
R.id.t3_widget_row,
"$label. $windowLabel. $percent. $reset. $detail"
)
return child
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package expo.modules.t3subscriptionwidget

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition
import org.json.JSONObject

class T3SubscriptionWidgetModule : Module() {
override fun definition() = ModuleDefinition {
Name("T3SubscriptionWidget")
Function("updateSnapshot") { snapshot: String ->
val context = appContext.reactContext ?: return@Function
JSONObject(snapshot) // Reject malformed writes before replacing the saved snapshot.
context.getSharedPreferences(SubscriptionUsageWidget.PREFERENCES, 0)
.edit().putString("snapshot", snapshot).apply()
SubscriptionUsageWidget.updateAll(context)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/t3_widget_background" />
<corners android:radius="20dp" />
</shape>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/t3_widget_root" android:layout_width="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:padding="16dp" android:background="@drawable/t3_subscription_widget_background">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/t3_subscription_widget_name" android:textStyle="bold" android:textSize="16sp" android:textColor="@color/t3_widget_foreground" android:maxLines="1" android:ellipsize="end" />
<LinearLayout android:id="@+id/t3_widget_rows" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="vertical">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" android:text="@string/t3_subscription_widget_empty" android:textSize="13sp" android:textColor="@color/t3_widget_secondary" />
</LinearLayout>
<TextView android:id="@+id/t3_widget_footer" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/t3_subscription_widget_open" android:textSize="10sp" android:textColor="@color/t3_widget_secondary" android:maxLines="1" android:ellipsize="end" />
</LinearLayout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/t3_widget_row" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingTop="8dp">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal">
<TextView android:id="@+id/t3_widget_label" android:layout_width="0dp" android:layout_weight="1" android:layout_height="wrap_content" android:textSize="12sp" android:textStyle="bold" android:textColor="@color/t3_widget_foreground" android:maxLines="1" android:ellipsize="end" />
<TextView android:id="@+id/t3_widget_percent" android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingStart="8dp" android:textSize="12sp" android:textColor="@color/t3_widget_foreground" />
</LinearLayout>
<TextView android:id="@+id/t3_widget_window" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="10sp" android:textColor="@color/t3_widget_secondary" android:maxLines="1" android:ellipsize="end" />
<ProgressBar android:id="@+id/t3_widget_progress" style="?android:attr/progressBarStyleHorizontal" android:layout_width="match_parent" android:layout_height="6dp" android:max="100" android:progressTint="#0284c7" />
<TextView android:id="@+id/t3_widget_reset" android:layout_width="match_parent" android:layout_height="wrap_content" android:textSize="10sp" android:textColor="@color/t3_widget_secondary" android:maxLines="1" android:ellipsize="end" />
</LinearLayout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<resources>
<color name="t3_widget_background">#18181B</color>
<color name="t3_widget_foreground">#FAFAFA</color>
<color name="t3_widget_secondary">#A1A1AA</color>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<resources>
<color name="t3_widget_background">#FAFAFA</color>
<color name="t3_widget_foreground">#18181B</color>
<color name="t3_widget_secondary">#52525B</color>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<resources>
<string name="t3_subscription_widget_unknown_check">Last checked unavailable</string>
<string name="t3_subscription_widget_name">Subscription usage</string>
<string name="t3_subscription_widget_description">Saved subscription quotas from your T3 Code environments. Tap to refresh in the app.</string>
<string name="t3_subscription_widget_empty">Open T3 Code and connect an environment to see limits.</string>
<string name="t3_subscription_widget_open">Tap to open Usage</string>
<string name="t3_subscription_widget_refresh">Open app to refresh</string>
<string name="t3_subscription_widget_remaining">%1$d%% remaining</string>
<string name="t3_subscription_widget_as_of">As of %1$s</string>
<string name="t3_subscription_widget_more"> · +%1$d more</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="250dp" android:minHeight="180dp"
android:minResizeWidth="180dp" android:minResizeHeight="130dp"
android:targetCellWidth="4" android:targetCellHeight="3"
android:resizeMode="horizontal|vertical" android:widgetCategory="home_screen"
android:updatePeriodMillis="1800000"
android:description="@string/t3_subscription_widget_description"
android:initialLayout="@layout/t3_subscription_widget"
android:previewLayout="@layout/t3_subscription_widget" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"platforms": ["android"],
"android": { "modules": ["expo.modules.t3subscriptionwidget.T3SubscriptionWidgetModule"] }
}
6 changes: 3 additions & 3 deletions apps/mobile/scripts/generate-uniwind-themes.mts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const color = (family: TailwindColorFamily, shade?: TailwindColorShade, opacity
// These replace the remaining dark:* utility pairs. A registered palette theme is
// neither literally `light` nor `dark`, so appearance-sensitive values must also be
// represented as semantic variables for custom themes.
const ADAPTIVE_COLORS = {
const ADAPTIVE_COLORS: Readonly<Record<string, readonly [light: string, dark: string]>> = {
"--color-adaptive-amber-50-950-a40": [color("amber", 50), color("amber", 950, 0.4)],
"--color-adaptive-amber-200-900-a60": [color("amber", 200), color("amber", 900, 0.6)],
"--color-adaptive-amber-500-a12-a16": [color("amber", 500, 0.12), color("amber", 500, 0.16)],
Expand Down Expand Up @@ -143,9 +143,9 @@ export const customThemeNames = BUILT_IN_THEME_IDS.flatMap((themeId) =>

const adaptiveVariablesFor = (appearance: MobileThemeAppearance) =>
Object.fromEntries(
Object.entries(ADAPTIVE_COLORS).map(([name, values]) => [
Object.entries(ADAPTIVE_COLORS).map(([name, [light, dark]]) => [
name,
values[appearance === "light" ? 0 : 1],
appearance === "light" ? light : dark,
]),
);

Expand Down
3 changes: 3 additions & 0 deletions apps/mobile/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { OverlayPortalHost } from "./components/OverlayPortal";
import { appBlurTargetRef } from "./lib/appBlurTarget";
import { useMobileNavigationTheme } from "./lib/useMobileNavigationTheme";

import { SubscriptionUsageCoordinator } from "./widgets/SubscriptionUsageCoordinator";

import "../global.css";

if (process.env.EXPO_PUBLIC_SHOWCASE === "1") {
Expand Down Expand Up @@ -77,6 +79,7 @@ function AppContent() {
return (
<>
<SplashScreenCoordinator />
<SubscriptionUsageCoordinator />
<GestureHandlerRootView className="flex-1">
<KeyboardProvider statusBarTranslucent>
<SafeAreaProvider>
Expand Down
8 changes: 8 additions & 0 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import { NewTaskFlowProvider } from "./features/threads/new-task-flow-provider";
import { NewTaskRouteScreen } from "./features/threads/NewTaskRouteScreen";
import { SettingsAppearanceRouteScreen } from "./features/settings/SettingsAppearanceRouteScreen";
import { SettingsClientStorageRouteScreen } from "./features/settings/SettingsClientStorageRouteScreen";
import { SettingsDiagnosticsRouteScreen } from "./features/diagnostics/SettingsDiagnosticsRouteScreen";
import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteScreen";
import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen";
import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen";
Expand Down Expand Up @@ -198,6 +199,13 @@ const SettingsContentStack = createNativeStackNavigator({
title: "Client Storage",
},
}),
SettingsDiagnostics: createNativeStackScreen({
screen: SettingsDiagnosticsRouteScreen,
linking: "diagnostics",
options: {
title: "Diagnostics",
},
}),
SettingsUsageAccount: createNativeStackScreen({
screen: UsageLimitAccountScreen,
options: { title: "Account" },
Expand Down
Loading
Loading