Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
62b1459
fix(mobile): transparent jet on the iOS splash screen
nickrroberts Sep 29, 2026
d5ae48c
fix(browser): reconnect thread automation after host timeout
nickrroberts Sep 29, 2026
97f47a1
chore(release): prepare Control Plane 0.0.57
nickrroberts Sep 29, 2026
1e1e569
Merge pull request #49 from lucive-apps/fix/thread-browser-reconnect
nickrroberts Sep 29, 2026
31287bd
Merge pull request #48 from lucive-apps/fix/ios-splash-transparent
nickrroberts Sep 29, 2026
8cf00a3
fix(server): let coordinators settle agents after follow-ups
nickrroberts Sep 29, 2026
f22abea
Merge pull request #47 from lucive-apps/feat/coordinator-owned-settling
nickrroberts Sep 29, 2026
13baae1
feat: start agents on linked machines per server settings
nickrroberts Sep 29, 2026
259c9bc
test: fix lint in agent machine tests
nickrroberts Sep 29, 2026
d7e07fd
feat: settle agents on linked machines
nickrroberts Sep 29, 2026
c057681
fix: leave remote agents open for follow-ups after they report
nickrroberts Sep 29, 2026
e79b4d2
Merge pull request #43 from lucive-apps/feat/multi-machine-agents
nickrroberts Sep 29, 2026
3d3613d
chore(release): prepare Control Plane 0.0.58
nickrroberts Sep 29, 2026
9296d3d
fix(mobile): list New Project with cached Projects on cold launch
nickrroberts Sep 30, 2026
eec2f20
Merge pull request #51 from lucive-apps/fix/mobile-home-load-flash
nickrroberts Sep 30, 2026
b4a74f6
Show settled Project agents only on the mobile Settled screen
nickrroberts Sep 30, 2026
ac1cc90
Merge pull request #53 from lucive-apps/fix/mobile-settled-only-in-se…
nickrroberts Sep 30, 2026
9d9d219
feat(mobile): add demo mode for App Store review
nickrroberts Sep 30, 2026
766c399
fix(mobile): friendly pairing error for unreachable or failing hosts
nickrroberts Sep 30, 2026
644115f
fix(connection): retry and explain failed pairing requests
nickrroberts Sep 30, 2026
739d72a
fix(connection): wait out a cold-starting server while pairing
nickrroberts Sep 30, 2026
4976c6f
feat(mobile): keep demo mode across app relaunches
nickrroberts Sep 30, 2026
e0bcca2
Merge pull request #52 from lucive-apps/feat/ios-demo-mode
nickrroberts Sep 30, 2026
ce52eb4
Require agents to use only the built-in Control Plane browser
nickrroberts Sep 30, 2026
c00ac77
Merge pull request #56 from lucive-apps/feat/agents-builtin-browser-only
nickrroberts Sep 30, 2026
3d3eb28
fix(server): allow explicitly requested desktop browser use
nickrroberts Sep 30, 2026
66555de
Merge pull request #57 from lucive-apps/feat/browser-explicit-user-ex…
nickrroberts Sep 30, 2026
e8d9d5f
feat: sync Projects and Tasks folder order across clients
nickrroberts Sep 29, 2026
a632207
test: cover project order key in snapshot fixtures and contracts; doc…
nickrroberts Sep 29, 2026
12c28bf
Merge pull request #50 from lucive-apps/feat/sync-sidebar-order
nickrroberts Sep 30, 2026
719e07e
fix(mobile): let wide markdown tables and code scroll horizontally
nickrroberts Sep 30, 2026
785c71f
Merge pull request #54 from lucive-apps/fix/mobile-table-horizontal-s…
nickrroberts Sep 30, 2026
a3533ab
fix(clients): a preview app no longer knocks the desktop's own server…
t3dotgg Sep 25, 2026
edf1095
fix(server): the SQLite WAL file shrinks back after large writes (#13…
t3dotgg Sep 26, 2026
67a974d
fix(server): background git status fetches no longer fill the disk wi…
t3dotgg Sep 26, 2026
14f4d28
perf(clients): saving the thread list cache no longer freezes the UI …
t3dotgg Sep 26, 2026
bfb8970
fix(server): OpenCode stop no longer hangs when a turn is interrupted…
SunkenInTime Sep 29, 2026
ef2dcd9
fix(web): skip unchanged shortcut modifier updates - 195x typing late…
callmemorgan Sep 28, 2026
b3e3266
fix(grok): recover from crashed provider sessions (#10607)
saphid Sep 30, 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 apps/desktop/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/desktop",
"version": "0.0.56",
"version": "0.0.58",
"private": true,
"type": "module",
"main": "dist-electron/main.cjs",
Expand Down
8 changes: 5 additions & 3 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ if (
const DEVELOPMENT_ASSETS = {
appIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng),
iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng),
splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng),
splashIcon: "./assets/ios-splash-icon-dev.png",
androidAdaptiveForeground: "./assets/android-icon-foreground-dev.png",
androidSplashIcon: "./assets/android-splash-icon-dev.png",
androidMonochromeIcon: "./assets/android-icon-mark.png",
Expand All @@ -46,7 +46,7 @@ const DEVELOPMENT_ASSETS = {
const PREVIEW_ASSETS = {
appIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng),
iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng),
splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng),
splashIcon: "./assets/ios-splash-icon-nightly.png",
androidAdaptiveForeground: "./assets/android-icon-foreground-nightly.png",
androidSplashIcon: "./assets/android-splash-icon-nightly.png",
androidMonochromeIcon: "./assets/android-icon-mark.png",
Expand All @@ -57,7 +57,7 @@ const PREVIEW_ASSETS = {
const RELEASE_ASSETS = {
appIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng),
iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng),
splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng),
splashIcon: "./assets/ios-splash-icon-prod.png",
androidAdaptiveForeground: "./assets/android-icon-foreground-prod.png",
androidSplashIcon: "./assets/android-splash-icon-prod.png",
androidMonochromeIcon: "./assets/android-icon-mark.png",
Expand Down Expand Up @@ -366,6 +366,8 @@ const config: ExpoConfig = {
[
"expo-splash-screen",
{
// iOS draws the splash image unmasked over backgroundColor, so it must be the
// transparent jet. The square app icon tile shows as a white box on the dark splash.
image: variant.assets.splashIcon,
resizeMode: "contain",
backgroundColor: "#ffffff",
Expand Down
Binary file added apps/mobile/assets/ios-splash-icon-dev.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile/assets/ios-splash-icon-nightly.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added apps/mobile/assets/ios-splash-icon-prod.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
131 changes: 115 additions & 16 deletions apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { createContext, memo, useContext, useMemo } from "react";
import { Image, Platform, ScrollView, Text, useColorScheme, View } from "react-native";
import { createContext, memo, type ReactNode, useContext, useMemo, useState } from "react";
import {
Image,
Platform,
ScrollView,
Text,
useColorScheme,
View,
type NativeScrollEvent,
type NativeSyntheticEvent,
type ViewStyle,
} from "react-native";
import type { MarkdownNode } from "react-native-nitro-markdown/headless";

import { CopyTextButton } from "./CopyTextButton";
import { horizontalOverflowEdges, horizontalOverflowFadeGradient } from "./horizontalOverflow";
import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";
import {
nativeMarkdownDocumentRuns,
Expand Down Expand Up @@ -145,6 +156,102 @@ function HighlightedCodeText(props: {
);
}

const OVERFLOW_FADE_WIDTH = 28;

/**
* Horizontal scroller for tables and code wider than the message.
*
* Fabric's ScrollView will not start a drag while any ancestor is the JS
* responder (`touchesShouldCancelInContentView` returns NO), and a message
* wrapped in a long-press Pressable takes the responder on every touch. While
* the content overflows, the content view claims the responder itself so the
* ancestor never does and the pan can scroll. Content that fits leaves touches
* to the ancestor, so its long press still works there.
*/
function HorizontalOverflowScroll(props: {
readonly children: ReactNode;
readonly contentStyle?: ViewStyle;
/** Opaque colour behind the content, faded in over any edge that hides some of it. */
readonly fadeColor?: string;
}) {
const [viewportWidth, setViewportWidth] = useState(0);
const [contentWidth, setContentWidth] = useState(0);
// Only whether an edge is hidden matters, so scroll events re-render when
// that flips rather than on every frame.
const [scrolled, setScrolled] = useState({ leading: false, trailing: true });
const { overflows } = horizontalOverflowEdges({ viewportWidth, contentWidth, offsetX: 0 });
const leadingFade =
overflows && scrolled.leading
? horizontalOverflowFadeGradient(props.fadeColor, "leading")
: undefined;
const trailingFade =
overflows && scrolled.trailing
? horizontalOverflowFadeGradient(props.fadeColor, "trailing")
: undefined;
const onScroll = overflows
? (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const { leading, trailing } = horizontalOverflowEdges({
viewportWidth,
contentWidth,
offsetX: event.nativeEvent.contentOffset.x,
});
setScrolled((previous) =>
previous.leading === leading && previous.trailing === trailing
? previous
: { leading, trailing },
);
}
: undefined;

return (
<View>
<ScrollView
horizontal
bounces={false}
nestedScrollEnabled={Platform.OS === "android"}
showsHorizontalScrollIndicator={false}
scrollEventThrottle={32}
onScroll={onScroll}
// Throttling can drop the event that lands on an edge; settle on the final offset.
onScrollEndDrag={onScroll}
onMomentumScrollEnd={onScroll}
onLayout={(event) => setViewportWidth(event.nativeEvent.layout.width)}
onContentSizeChange={(width) => setContentWidth(width)}
>
<View style={props.contentStyle} onStartShouldSetResponder={() => overflows}>
{props.children}
</View>
</ScrollView>
{leadingFade ? (
<View
pointerEvents="none"
style={{
position: "absolute",
top: 0,
bottom: 0,
left: 0,
width: OVERFLOW_FADE_WIDTH,
experimental_backgroundImage: leadingFade,
}}
/>
) : null}
{trailingFade ? (
<View
pointerEvents="none"
style={{
position: "absolute",
top: 0,
bottom: 0,
right: 0,
width: OVERFLOW_FADE_WIDTH,
experimental_backgroundImage: trailingFade,
}}
/>
) : null}
</View>
);
}

function NativeCodeBlock(props: {
readonly node: MarkdownNode;
readonly textStyle: NativeMarkdownTextStyle;
Expand Down Expand Up @@ -204,19 +311,16 @@ function NativeCodeBlock(props: {
iconSize={14}
/>
</View>
<ScrollView
horizontal
bounces={false}
nestedScrollEnabled={Platform.OS === "android"}
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 14, paddingVertical: 12 }}
<HorizontalOverflowScroll
contentStyle={{ paddingHorizontal: 14, paddingVertical: 12 }}
fadeColor={props.textStyle.codeBlockSurfaceColor}
>
<HighlightedCodeText
content={content}
highlighted={highlighted}
textStyle={props.textStyle}
/>
</ScrollView>
</HorizontalOverflowScroll>
</View>
);
}
Expand Down Expand Up @@ -244,12 +348,7 @@ function NativeTable(props: {
}) {
const rows = collectTableRows(props.node);
return (
<ScrollView
horizontal
bounces={false}
nestedScrollEnabled={Platform.OS === "android"}
showsHorizontalScrollIndicator={false}
>
<HorizontalOverflowScroll fadeColor={props.textStyle.surfaceColor}>
<View
style={{
borderColor: props.textStyle.dividerColor,
Expand Down Expand Up @@ -292,7 +391,7 @@ function NativeTable(props: {
</View>
))}
</View>
</ScrollView>
</HorizontalOverflowScroll>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ export interface NativeMarkdownTextStyle {
readonly quoteMarkerColor: string;
readonly dividerColor: string;
readonly contextChipBorderColor?: string;
/** Opaque colour the markdown sits on; wide tables fade into it at a scrollable edge. */
readonly surfaceColor?: string;
/** Opaque colour behind code block text; wide code fades into it at a scrollable edge. */
readonly codeBlockSurfaceColor?: string;
readonly fontSize: number;
readonly lineHeight: number;
readonly fontFamily: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { expect, it } from "vite-plus/test";

import { horizontalOverflowEdges, horizontalOverflowFadeGradient } from "./horizontalOverflow";

it("leaves a table that fits its viewport static", () => {
expect(horizontalOverflowEdges({ viewportWidth: 363, contentWidth: 322, offsetX: 0 })).toEqual({
overflows: false,
leading: false,
trailing: false,
});
// Rounding in the measured widths is not scrollable content.
expect(
horizontalOverflowEdges({ viewportWidth: 363, contentWidth: 363.5, offsetX: 0 }).overflows,
).toBe(false);
});

it("marks the hidden edges of a wide table as it scrolls", () => {
const wide = { viewportWidth: 363, contentWidth: 802 };
expect(horizontalOverflowEdges({ ...wide, offsetX: 0 })).toEqual({
overflows: true,
leading: false,
trailing: true,
});
expect(horizontalOverflowEdges({ ...wide, offsetX: 200 })).toEqual({
overflows: true,
leading: true,
trailing: true,
});
expect(horizontalOverflowEdges({ ...wide, offsetX: 439 })).toEqual({
overflows: true,
leading: true,
trailing: false,
});
});

it("does not treat an unmeasured viewport as overflowing", () => {
expect(
horizontalOverflowEdges({ viewportWidth: 0, contentWidth: 802, offsetX: 0 }).overflows,
).toBe(false);
});

it("fades toward the hidden edge only for opaque hex surfaces", () => {
expect(horizontalOverflowFadeGradient("#f2f2f7", "trailing")).toBe(
"linear-gradient(to right, #f2f2f700 0%, #f2f2f7 100%)",
);
expect(horizontalOverflowFadeGradient("#0A0A0A", "leading")).toBe(
"linear-gradient(to left, #0A0A0A00 0%, #0A0A0A 100%)",
);
expect(horizontalOverflowFadeGradient("rgba(0, 0, 0, 0.04)", "trailing")).toBeUndefined();
expect(horizontalOverflowFadeGradient(undefined, "trailing")).toBeUndefined();
});
39 changes: 39 additions & 0 deletions apps/mobile/modules/t3-markdown-text/src/horizontalOverflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/** Sub-point differences are layout rounding, not scrollable content. */
const OVERFLOW_TOLERANCE = 1;

export interface HorizontalOverflowEdges {
/** Content is wider than the viewport, so the block scrolls. */
readonly overflows: boolean;
/** Content is hidden past the leading edge. */
readonly leading: boolean;
/** Content is hidden past the trailing edge. */
readonly trailing: boolean;
}

export function horizontalOverflowEdges(input: {
readonly viewportWidth: number;
readonly contentWidth: number;
readonly offsetX: number;
}): HorizontalOverflowEdges {
const maxOffset = input.contentWidth - input.viewportWidth;
const overflows = input.viewportWidth > 0 && maxOffset > OVERFLOW_TOLERANCE;
return {
overflows,
leading: overflows && input.offsetX > OVERFLOW_TOLERANCE,
trailing: overflows && input.offsetX < maxOffset - OVERFLOW_TOLERANCE,
};
}

/**
* CSS gradient that fades content into an opaque surface at one edge. Only
* six-digit hex colours can take the transparent `00` alpha suffix, so other
* formats get no fade rather than a gradient through transparent black.
*/
export function horizontalOverflowFadeGradient(
color: string | undefined,
edge: "leading" | "trailing",
): string | undefined {
if (color === undefined || !/^#[0-9a-f]{6}$/i.test(color)) return undefined;
const direction = edge === "leading" ? "to left" : "to right";
return `linear-gradient(${direction}, ${color}00 0%, ${color} 100%)`;
}
Loading
Loading