Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
444fd8b
fix(mobile): keep Android markdown icons aligned with text (#11079)
SunkenInTime Sep 10, 2026
3836890
Revert "fix(mobile): keep Android markdown icons aligned with text" (…
juliusmarminge Sep 10, 2026
e784975
fix(ui): simplify multiple linked pull request badges (#11104)
maria-rcks Sep 10, 2026
0882431
fix(preview): return to pip when closing the right panel (#11102)
maria-rcks Sep 10, 2026
0527ddf
fix: quiet settled threads and simplify PR badges (#11101)
juliusmarminge Sep 10, 2026
f814983
fix(web): emphasize primary pull request actions (#11105)
juliusmarminge Sep 10, 2026
21a5ccf
fix(web): prevent seams in the topbar scroll fade (#10914)
caezium Sep 10, 2026
21d7440
fix(web): fit provider update text inside sidebar notices (#11034)
MatthewFeroz Sep 10, 2026
3997b9a
fix(web): align floating browser preview corners (#10915)
caezium Sep 10, 2026
60eff99
fix(web): save PR body edits with Cmd/Ctrl+Enter (#10660)
flamboh Sep 10, 2026
502131a
fix(web): collapse a tool call by clicking its expanded label (#11017)
maria-rcks Sep 10, 2026
dca7b59
feat(devices): add simulator and emulator support (#10677)
juliusmarminge Sep 10, 2026
e022fa4
feat(devices): scope targets and sessions to their hosts (#10854)
juliusmarminge Sep 10, 2026
7734c6d
feat(devices): target concurrent agent sessions across hosts (#10855)
juliusmarminge Sep 10, 2026
d2eeacd
feat(devices): connect simulator hosts over SSH (#10856)
juliusmarminge Sep 10, 2026
dfa345b
feat(web): use a compact right-panel surface menu (#11111)
maria-rcks Sep 10, 2026
32b6909
fix(mobile): keep Android markdown icons aligned (#11118)
none23 Sep 10, 2026
47dbb06
fix(mobile): add close controls to tablet files and terminal (#11115)
juliusmarminge Sep 10, 2026
96f4370
fix(mobile): preserve the final composer animation frame (#11114)
juliusmarminge Sep 10, 2026
859304b
fix(mobile): keep composer transitions aligned (#11127)
juliusmarminge Sep 10, 2026
1a9336b
refactor(mobile): name shared markdown renderer without iOS suffixes …
SunkenInTime Sep 10, 2026
a5ac766
fix(media): preserve playback during fullscreen transitions (#11113)
maria-rcks Sep 10, 2026
39ca417
fix(marketing): redirect /app to app.t3.codes (#11145)
t3-code[bot] Sep 10, 2026
2afa02a
chore(marketing): update to 300k users and 22k stars (#11146)
t3-code[bot] Sep 10, 2026
c52b8d9
feat(command-palette): show environments in search results (#10722)
Cyberlane Sep 10, 2026
6a2d246
fix(pr): update labels and reviewers without redundant reloads (#11117)
maria-rcks Sep 11, 2026
27eb79d
fix(chat): fold question answers into tool activity (#11014)
maria-rcks Sep 11, 2026
48654c1
fix(usage): flag unpriced model activity instead of showing $0.00 (#1…
maria-rcks Sep 11, 2026
5735693
fix(server): let Claude launch args override the derived permission m…
maria-rcks Sep 11, 2026
20ef250
fix(editors): accept root paths and Windows servers in Zed remote lin…
maria-rcks Sep 11, 2026
4d06156
fix(web): center pull request unavailable states (#11110)
maria-rcks Sep 11, 2026
0a37240
fix(web): remove sidebar pull request link icon (#11179)
maria-rcks Sep 11, 2026
02297e3
fix(ui): color linked pr counts by aggregate status (#11180)
maria-rcks Sep 11, 2026
6c69534
fix(preview): render website favicons for browser tool activity (#11032)
maria-rcks Sep 11, 2026
18c5a1d
fix(web): simplify pull request summary sections (#10612)
maria-rcks Sep 11, 2026
ef6fa11
fix(web): preserve drafts when compacting context (#11103)
maria-rcks Sep 11, 2026
57aee3e
fix(server): queue messages during context compaction (#11107)
maria-rcks Sep 11, 2026
8fc2536
perf(web): format minimap previews only when opened (#11181)
juliusmarminge Sep 11, 2026
a9dabbf
perf(web): reuse completed Markdown prefixes while streaming (#11193)
juliusmarminge Sep 11, 2026
d7d7f8f
perf(web): resume syntax highlighting from completed lines (#11196)
juliusmarminge Sep 11, 2026
8078c53
perf(web): preserve completed code-line DOM while streaming (#11198)
juliusmarminge Sep 11, 2026
211618f
perf(web): huge-thread switch no longer blanks the chat pane (#11169)
juliusmarminge Sep 11, 2026
fb3d165
fix(web): show platform file manager icons in Open menu (#11228)
Bil0000 Sep 11, 2026
26894dd
fix(server): detect file renames in review diffs (#8086)
jakeleventhal Sep 11, 2026
5eecc24
fix(cli): pin shared Effect dependency for npm installs (#11240)
jakeleventhal Sep 11, 2026
2ebc9fa
fix(mobile): prevent Hermes crashes when opening threads (#11233)
jakeleventhal Sep 11, 2026
05d4042
feat(web): open Usage on the Limits tab by default (#11261)
juliusmarminge Sep 11, 2026
2b7d3a4
perf(web): avoid scanning chat history for sidebar backgrounds (#11206)
juliusmarminge Sep 11, 2026
7bd7f99
perf(mobile): reuse completed code lines while streaming (#11211)
juliusmarminge Sep 11, 2026
6e8931d
perf(client): reduce remote request and message sync overhead (#11029)
Bil0000 Sep 11, 2026
e1c94f7
fix(web): refresh usage limit countdowns without switching tabs (#11187)
t3-code[bot] Sep 11, 2026
fb52d12
fix(client-runtime): typecheck device hub ticket request on main (#11…
juliusmarminge Sep 11, 2026
2c0e891
feat(settings): add per-project overrides for scopable server setting…
juliusmarminge Sep 11, 2026
8b2c046
feat(web): pick settings environment and project as two selects (#10636)
juliusmarminge Sep 11, 2026
e22040d
feat(settings): edit any scopable setting as a project override (#10639)
juliusmarminge Sep 11, 2026
8bbe2bf
feat(web): float device streams over chat (#11285)
juliusmarminge Sep 11, 2026
18f7254
fix(web): floating preview can use the margins beside the composer (#…
juliusmarminge Sep 11, 2026
48648a9
chore: sync upstream main
matheustimbo Sep 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 9 additions & 3 deletions apps/desktop/src/electron/ElectronShell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,16 @@ describe("ElectronShell", () => {
openExternalMock.mockResolvedValue(undefined);

const electronShell = yield* ElectronShell.ElectronShell;
const result = yield* electronShell.openExternal("zed://ssh/example.com/home/user/project");
const results = yield* Effect.all([
electronShell.openExternal("zed://ssh/example.com/home/user/project"),
electronShell.openExternal("zed://ssh/example.com/"),
]);

assert.equal(result, true);
assert.deepEqual(openExternalMock.mock.calls, [["zed://ssh/example.com/home/user/project"]]);
assert.deepEqual(results, [true, true]);
assert.deepEqual(openExternalMock.mock.calls, [
["zed://ssh/example.com/home/user/project"],
["zed://ssh/example.com/"],
]);
}).pipe(Effect.provide(ElectronShell.layer)),
);

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/electron/ElectronShell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const REMOTE_EDITOR_PROTOCOLS = new Set(
);

// Zed's host sits in the first path segment, so it needs its own userinfo ban.
const ZED_SSH_PATHNAME = /^\/[^/@:]+\/.+$/;
const ZED_SSH_PATHNAME = /^\/[^/@:]+\/.*$/;

const isRemoteEditorUrl = (url: URL) =>
REMOTE_EDITOR_PROTOCOLS.has(url.protocol) &&
Expand Down
4 changes: 2 additions & 2 deletions apps/marketing/src/lib/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ export const ANDROID_PLAY_STORE_URL =
"https://play.google.com/store/apps/details?id=com.t3tools.t3code";

export const MARKETING_STATS = {
githubStars: "21k+",
users: "200,000",
githubStars: "22k+",
users: "300,000",
} as const;
7 changes: 7 additions & 0 deletions apps/marketing/vercel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@ export const config: VercelConfig = {
installCommand: "npm install -g vite-plus && vp install --filter '@t3tools/marketing...'",
buildCommand: "vp run --filter @t3tools/marketing build",
outputDirectory: "dist",
redirects: [
{
source: "/app",
destination: "https://app.t3.codes",
permanent: true,
},
],
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package expo.modules.t3markdowntext
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.ReplacementSpan
import android.view.ActionMode
Expand All @@ -18,6 +20,14 @@ import kotlin.math.min

private const val OBJECT_REPLACEMENT_CHARACTER = "\uFFFC"

// Match React Native's measurement buffer. Android orders tied line-height
// spans differently in SpannableString, shifting inline images once RN's
// span priorities are exhausted.
private object MarkdownSpannableFactory : Spannable.Factory() {
override fun newSpannable(source: CharSequence): Spannable =
SpannableStringBuilder(source)
}

private fun copyTextWithoutInlineImages(
text: CharSequence,
start: Int,
Expand Down Expand Up @@ -85,6 +95,7 @@ class T3MarkdownTextSelectionModule : Module() {
if (currentCallback is SanitizingSelectionActionModeCallback) {
return@runOnUiQueueThread
}
textView.setSpannableFactory(MarkdownSpannableFactory)
textView.customSelectionActionModeCallback =
SanitizingSelectionActionModeCallback(textView, currentCallback)
}
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/modules/t3-markdown-text/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"./links": "./src/markdownLinks.ts",
"./markdown": "./src/nativeMarkdownText.ts",
"./primitive": "./src/MarkdownTextPrimitive.tsx",
"./renderer": "./src/SelectableMarkdownText.ios.tsx",
"./renderer": "./src/SelectableMarkdownText.tsx",
"./types": "./src/SelectableMarkdownText.types.ts"
},
"peerDependencies": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,35 +1,35 @@
import { createContext, useContext, useEffect, useState } from "react";
import { createContext, memo, useContext, useMemo } from "react";
import { Image, Platform, ScrollView, Text, useColorScheme, View } from "react-native";
import type { MarkdownNode } from "react-native-nitro-markdown/headless";

import { CopyTextButton } from "./CopyTextButton";
import { MarkdownTextPrimitive } from "./MarkdownTextPrimitive";
import { nativeMarkdownDocumentRuns, nativeMarkdownListItemBlocks } from "./nativeMarkdownText";
import { NativeMarkdownSelectableText } from "./NativeMarkdownSelectableText.ios";
import {
nativeMarkdownDocumentRuns,
nativeMarkdownListItemBlocks,
nativeMarkdownNodePosition,
} from "./nativeMarkdownText";
import { NativeMarkdownSelectableText } from "./NativeMarkdownSelectableText";
import type {
MarkdownCodeHighlighter,
MarkdownHighlightedToken,
MarkdownImageRenderer,
NativeMarkdownTextStyle,
SelectableMarkdownSkill,
} from "./SelectableMarkdownText.types";
import { useHighlightedCode, type HighlightedCode } from "./useHighlightedCode";

/** Set by SelectableMarkdownText so images anywhere in the block tree can use it. */
export const MarkdownImageRendererContext = createContext<MarkdownImageRenderer | null>(null);

type HighlightedCode = ReadonlyArray<ReadonlyArray<MarkdownHighlightedToken>>;

const highlightedCodeCache = new Map<string, HighlightedCode>();
const highlightedCodePromiseCache = new Map<string, Promise<HighlightedCode>>();
const HIGHLIGHTED_CODE_CACHE_LIMIT = 64;
const MONO_FONT_FAMILY = Platform.select({
ios: "ui-monospace",
android: "monospace",
default: "monospace",
});

function nodeKey(node: MarkdownNode, index: number): string {
return `${node.type}:${node.beg ?? index}:${node.end ?? index}`;
return `${node.type}:${nativeMarkdownNodePosition(node, index)}`;
}

/** Code inside markdown scales with the base text size (12pt at the default 15pt body). */
Expand Down Expand Up @@ -67,174 +67,74 @@ function SelectableNode(props: {
);
}

function codeHighlightCacheKey(
code: string,
language: string | undefined,
theme: "light" | "dark",
): string {
return `${theme}:${language ?? "text"}:${code}`;
}

function cacheHighlightedCode(key: string, tokens: HighlightedCode): void {
highlightedCodeCache.delete(key);
highlightedCodeCache.set(key, tokens);

while (highlightedCodeCache.size > HIGHLIGHTED_CODE_CACHE_LIMIT) {
const oldestKey = highlightedCodeCache.keys().next().value;
if (oldestKey === undefined) {
break;
}
highlightedCodeCache.delete(oldestKey);
}
}

function loadHighlightedCode(
code: string,
language: string | undefined,
theme: "light" | "dark",
highlightCode: MarkdownCodeHighlighter,
): Promise<HighlightedCode> {
const key = codeHighlightCacheKey(code, language, theme);
const cached = highlightedCodeCache.get(key);
if (cached) {
return Promise.resolve(cached);
}

const pending = highlightedCodePromiseCache.get(key);
if (pending) {
return pending;
}

const promise = highlightCode({ code, language, theme })
.then((tokens) => {
cacheHighlightedCode(key, tokens);
highlightedCodePromiseCache.delete(key);
return tokens;
})
.catch((error) => {
highlightedCodePromiseCache.delete(key);
throw error;
});
highlightedCodePromiseCache.set(key, promise);
return promise;
}

function useHighlightedCode(
code: string,
language: string | undefined,
theme: "light" | "dark",
highlightCode: MarkdownCodeHighlighter,
): HighlightedCode | null {
const key = codeHighlightCacheKey(code, language, theme);
const [highlighted, setHighlighted] = useState<{
readonly key: string;
readonly tokens: HighlightedCode | null;
}>(() => ({
key,
tokens: highlightedCodeCache.get(key) ?? null,
}));

useEffect(() => {
let active = true;
const cached = highlightedCodeCache.get(key);
if (cached) {
cacheHighlightedCode(key, cached);
setHighlighted({ key, tokens: cached });
return () => {
active = false;
};
}

void loadHighlightedCode(code, language, theme, highlightCode)
.then((tokens) => {
if (active) {
setHighlighted({ key, tokens });
}
})
.catch(() => {
if (active) {
setHighlighted({ key, tokens: null });
}
});
return () => {
active = false;
};
}, [code, highlightCode, key, language, theme]);

return highlighted.key === key ? highlighted.tokens : null;
}

function HighlightedCodeText(props: {
readonly content: string;
readonly highlighted: HighlightedCode | null;
readonly textStyle: NativeMarkdownTextStyle;
const HighlightedCodeLine = memo(function HighlightedCodeLine(props: {
readonly tokens: ReadonlyArray<MarkdownHighlightedToken>;
readonly color: string;
readonly newline: boolean;
}) {
if (!props.highlighted) {
return (
let offset = 0;
const children = [];
for (const token of props.tokens) {
if (!token.content) continue;
children.push(
<MarkdownTextPrimitive
uiTextView
selectable
key={offset}
style={{
color: props.textStyle.codeColor,
color: token.color ?? props.color,
fontFamily: MONO_FONT_FAMILY,
fontSize: codeBlockFontSize(props.textStyle),
lineHeight: codeBlockLineHeight(props.textStyle),
fontStyle: token.fontStyle !== null && (token.fontStyle & 1) === 1 ? "italic" : "normal",
fontWeight: token.fontStyle !== null && (token.fontStyle & 2) === 2 ? "700" : "400",
}}
>
{props.content}
</MarkdownTextPrimitive>
{token.content}
</MarkdownTextPrimitive>,
);
offset += token.content.length;
}
const highlighted = props.highlighted;
let sourceOffset = 0;
const keyOccurrences = new Map<string, number>();
const keyedLines = highlighted.map((line) => {
const lineStart = sourceOffset;
const tokens = line.map((token) => {
const start = sourceOffset;
sourceOffset += token.content.length;
const signature = `${start}:${token.content}:${token.color ?? ""}:${token.fontStyle ?? ""}`;
const occurrence = keyOccurrences.get(signature) ?? 0;
keyOccurrences.set(signature, occurrence + 1);
return { key: `${signature}:${occurrence}`, token };
});
sourceOffset += 1;
return {
key: `line:${lineStart}:${line.map((token) => token.content).join("")}`,
tokens,
};
});
return (
<MarkdownTextPrimitive>
{children}
{props.newline ? "\n" : ""}
</MarkdownTextPrimitive>
);
});

function HighlightedCodeText(props: {
readonly content: string;
readonly highlighted: HighlightedCode | null;
readonly textStyle: NativeMarkdownTextStyle;
}) {
// The text root provides inherited styles through context. A new style object
// would rerender every token even when its completed line is unchanged.
const fontSize = codeBlockFontSize(props.textStyle);
const lineHeight = codeBlockLineHeight(props.textStyle);
const style = useMemo(
() => ({
color: props.textStyle.codeColor,
fontFamily: MONO_FONT_FAMILY,
fontSize,
lineHeight,
}),
[props.textStyle.codeColor, fontSize, lineHeight],
);
let offset = 0;
const lines = [];
if (props.highlighted) {
for (const tokens of props.highlighted) {
lines.push(
<HighlightedCodeLine
key={offset}
tokens={tokens}
color={props.textStyle.codeColor}
newline={lines.length + 1 < props.highlighted.length}
/>,
);
offset += tokens.reduce((length, token) => length + token.content.length, 0) + 1;
}
}
return (
<MarkdownTextPrimitive
uiTextView
selectable
style={{
color: props.textStyle.codeColor,
fontFamily: MONO_FONT_FAMILY,
fontSize: codeBlockFontSize(props.textStyle),
lineHeight: codeBlockLineHeight(props.textStyle),
}}
>
{keyedLines.map((line, lineIndex) => (
<MarkdownTextPrimitive key={line.key}>
{line.tokens.map(({ key, token }) => (
<MarkdownTextPrimitive
key={key}
style={{
color: token.color ?? props.textStyle.codeColor,
fontFamily: MONO_FONT_FAMILY,
fontStyle:
token.fontStyle !== null && (token.fontStyle & 1) === 1 ? "italic" : "normal",
fontWeight: token.fontStyle !== null && (token.fontStyle & 2) === 2 ? "700" : "400",
}}
>
{token.content}
</MarkdownTextPrimitive>
))}
{lineIndex + 1 < keyedLines.length ? "\n" : ""}
</MarkdownTextPrimitive>
))}
<MarkdownTextPrimitive uiTextView selectable style={style}>
{props.highlighted ? lines : props.content}
</MarkdownTextPrimitive>
);
}
Expand Down
Loading
Loading