Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
da976cf
fix(marketing): restore continuous endorsement scrolling (#10450)
t3dotgg Sep 7, 2026
bd280de
Revert "fix(marketing): restore continuous endorsement scrolling" (#1…
t3dotgg Sep 7, 2026
b22646c
fix(marketing): bring back the endorsement marquee (#10455)
t3dotgg Sep 7, 2026
de28fa1
chore: enable CodeRabbit automatic reviews (#10457)
t3dotgg Sep 7, 2026
1c1d38f
fix(codex): keep Spark limits from replacing the main allowance (#10458)
Yash-Singh1 Sep 7, 2026
7e03dcf
fix(marketing): send 95 nightly downloads to the downloads page (#10460)
t3dotgg Sep 7, 2026
ecf3716
fix(web): composer regains focus when you tab back into T3 Code (#10463)
t3dotgg Sep 7, 2026
4e969f3
fix(web): keep sidebar drag dividers clear and gestures smooth (#10453)
juliusmarminge Sep 7, 2026
6134b90
fix(server): mark Cursor transport error answers as failed (#10337)
shivamhwp Sep 7, 2026
f5fb056
fix(web): clear stuck panel resize cursor (#10461)
t3dotgg Sep 7, 2026
490eb17
fix(web): clarify sidebar drag dividers and empty targets (#10464)
juliusmarminge Sep 7, 2026
f729e8f
fix(web): make onboarding a shared multi-computer wizard (#10465)
juliusmarminge Sep 7, 2026
6abdf37
fix(shared): redeem reset credits through the hub when it holds the a…
juliusmarminge Sep 7, 2026
e1230d6
fix(mobile): keep pending messages in the chat timeline (#10449)
juliusmarminge Sep 7, 2026
bb5748b
fix(mobile): show connection status in the floating pill instead of a…
juliusmarminge Sep 7, 2026
a7028f1
fix: use Pierre icons consistently for attachments (#10475)
juliusmarminge Sep 7, 2026
08c715e
chore(release): prepare v0.0.39
t3-code[bot] Sep 7, 2026
5b68b2c
feat(mobile): open the thread screen as soon as a new task is submitt…
juliusmarminge Sep 7, 2026
7376536
fix(devcontainer): make repository setup work (#7875)
saphid Sep 7, 2026
d8bc683
fix(projects): prevent invalid script IDs from crashing threads (#10019)
saphid Sep 7, 2026
bc3dc26
fix(mobile): hide changed-files navigator and restore refresh in raw …
lnieuwenhuis Sep 7, 2026
e3b644c
fix(ios): scroll short source files from blank space (#10178)
juliusmarminge Sep 7, 2026
7dda0b1
feat(mobile): start a new thread on an existing branch (#10359)
StiensWout Sep 7, 2026
062987b
fix(mobile): improve font-size slider performance and prevent maximum…
bbernag Sep 7, 2026
b919d63
fix(web): keep composer toolbar controls anchored during transitions …
juliusmarminge Sep 7, 2026
f57d383
fix(web): resize the floating preview from any edge (#10467)
juliusmarminge Sep 7, 2026
7129797
fix(mobile): prevent chat from disappearing when scrolling (#10479)
juliusmarminge Sep 7, 2026
b248f5a
fix(mobile): smooth composer status pill resizing (#10484)
juliusmarminge Sep 7, 2026
c0d4e95
fix(mobile): release initial scroll target after dragging (#10483)
juliusmarminge Sep 7, 2026
e32dd42
fix(mobile): animate thread lifecycle transitions consistently (#10487)
juliusmarminge Sep 7, 2026
b717537
fix(mobile): restore assistant message bottom padding (#10491)
juliusmarminge Sep 7, 2026
dc39615
fix(mobile): preserve chat rows when toggling commands (#10492)
juliusmarminge Sep 7, 2026
8b2838e
feat(web): group onboarding project import by repository (#10493)
t3dotgg Sep 7, 2026
4f87747
chore(upstream): merge pingdotgg/t3code@8b2838e0
claude Sep 7, 2026
0c81071
fix(web): keep the onboarding seam test inside the fork's own rules
claude Sep 7, 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 .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
reviews:
review_status: false
auto_review:
enabled: false
enabled: true
91 changes: 72 additions & 19 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -1,32 +1,85 @@
// Dev container for Marcode. Mirrors CI (ubuntu-24.04, Node 24, Rust stable)
// and the canonical setup in docs/operations/development.md: global `vp`, `vp i`.
// Contributor doc: docs/internals/devcontainer.md
{
// Marcode fork seam: upstream names this container "T3 Code". Product
// identity is Marcode's; everything else in this file follows upstream.
"name": "Marcode Dev",
"image": "debian:bookworm",
"image": "mcr.microsoft.com/devcontainers/base:ubuntu-24.04",
"features": {
"ghcr.io/devcontainers/features/git:1": {},
// nodeGypDependencies (default true) brings python3/make/g++, which Linux
// needs for node-pty's node-gyp fallback (its prebuilds are mac/win only).
"ghcr.io/devcontainers/features/node:2": {
"version": "24"
},
// native/resource-monitor (edition 2024, needs stable >= 1.85). The server
// degrades gracefully without the binary, but CI checks cargo fmt + test.
"ghcr.io/devcontainers/features/rust:1": {},
"ghcr.io/devcontainers/features/github-cli:1": {},
// Marcode-only: not the package manager (that is pnpm via vp), but what
// AGENTS.md uses for `bun:sqlite` test-data seeding.
"ghcr.io/devcontainers-extra/features/bun:1": {
"version": "1.3.11"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "24.13.1"
},
"ghcr.io/devcontainers/features/python:1": {
"version": "3.10",
"installTools": false
}
},
"overrideFeatureInstallOrder": [
"ghcr.io/devcontainers/features/git",
"ghcr.io/devcontainers-extra/features/bun"
"hostRequirements": {
"cpus": 4,
"memory": "8gb",
"storage": "32gb"
},
"containerEnv": {
// Keep all runtime state inside the (gitignored) workspace .marcode,
// matching the worktree default. An explicit --home-dir still wins.
//
// Marcode fork seam: upstream exports T3CODE_HOME here. Marcode's server
// reads MARCODE_HOME (packages/shared/src/forkIdentity.ts); the upstream
// name would be silently ignored and send dev state at the shared home.
"MARCODE_HOME": "${containerWorkspaceFolder}/.marcode"
},
"mounts": [
// vp keeps the pnpm content-addressable store and metadata cache under
// ~/.cache/pnpm (verified: the v11 store dir lives there); mounting a
// volume there lets installs survive container rebuilds.
"source=t3code-pnpm-store,target=/home/vscode/.cache/pnpm,type=volume",
// Root node_modules holds the whole pnpm virtual store (.pnpm), so one
// volume keeps the heavy tree off the slow macOS/Windows bind mount.
// Scoped by devcontainerId so parallel checkouts do not share it.
// Codespaces note: prebuild snapshots exclude volumes; drop these mounts
// if prebuilt codespaces become the primary workflow.
"source=t3code-node-modules-${devcontainerId},target=${containerWorkspaceFolder}/node_modules,type=volume"
],
"postCreateCommand": {
// corepack reads the pnpm version from package.json `packageManager`, so this
// never drifts from the lockfile. bun stays installed above: it is not the
// package manager, it is what AGENTS.md uses for `bun:sqlite` test-data seeding.
"install": "corepack enable && pnpm install --frozen-lockfile"
"onCreateCommand": "bash .devcontainer/on-create.sh",
"updateContentCommand": "bash .devcontainer/update-content.sh",
"forwardPorts": [5733, 13773],
"portsAttributes": {
"5733": {
"label": "marcode web (open via the pairing URL, not the bare origin)",
"onAutoForward": "notify"
},
"13773": {
"label": "marcode server",
"onAutoForward": "silent"
}
},
"customizations": {
"codespaces": {
"openFiles": ["docs/internals/devcontainer.md"]
},
"vscode": {
"extensions": ["oxc.oxc-vscode"]
"extensions": ["oxc.oxc-vscode", "rust-lang.rust-analyzer"],
"settings": {
// .repos is a large vendored read-only reference tree; watching it
// burns CPU and file handles.
"files.watcherExclude": {
"**/.repos/**": true,
"**/.marcode/**": true,
"**/.t3/**": true
},
"search.exclude": {
"**/.repos": true
}
}
}
}
},
"remoteUser": "vscode"
}
38 changes: 38 additions & 0 deletions .devcontainer/on-create.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# One-time container setup, baked into prebuilds. Content-dependent work
# (dependency install, Chromium) lives in update-content.sh.
set -euo pipefail

# The Vite+ CLI is the repo task runner (vp i, vp run dev, vp test run).
# Download to a file first: a curl failure inside $( ) would yield an empty
# script and a false success. VP_NODE_MANAGER=no skips the installer's node
# shims; Node comes from the devcontainer feature. The VP_*_DIR group (set
# together, absolute) pins the install location instead of the installer's
# XDG platform defaults, which moved the binary from ~/.vite-plus/bin to
# ~/.local/share/vite-plus/bin between releases and would break the check
# below either way.
export VP_BIN_DIR="$HOME/.local/share/vite-plus/bin"
export VP_DATA_DIR="$HOME/.local/share/vite-plus"
export VP_CACHE_DIR="$HOME/.cache/vite-plus"
installer=$(mktemp)
curl -fsSL https://vite.plus -o "$installer"
VP_NODE_MANAGER=no bash "$installer"
rm -f "$installer"

# Non-login lifecycle shells never source the profile the installer edits,
# so expose vp on the default PATH. test -x keeps a bad install loud.
test -x "$VP_BIN_DIR/vp"
sudo ln -sf "$VP_BIN_DIR/vp" /usr/local/bin/vp

# First-run terminal notice, rendered by the devcontainers base image.
sudo mkdir -p /usr/local/etc/vscode-dev-containers
sudo tee /usr/local/etc/vscode-dev-containers/first-run-notice.txt >/dev/null <<'EOF'
Marcode devcontainer

vp run dev start server + web, then open the pairing URL it
prints (the bare forwarded port will not authenticate)
cp .env.example .env optional: enable Marcode Connect cloud features
(public identifiers, not secrets)

Details: docs/internals/devcontainer.md
EOF
19 changes: 19 additions & 0 deletions .devcontainer/update-content.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Runs at creation and on every prebuild content refresh, so codespaces start
# with deps installed and caches warm. Everything here is idempotent.
set -euo pipefail

# Volume mounts (pnpm store, node_modules) and the directories docker creates
# for them arrive root-owned; hand them to the dev user before installing.
for dir in "$HOME/.cache" "$HOME/.cache/pnpm" node_modules; do
if [ -d "$dir" ] && [ "$(stat -c %U "$dir")" != "$(id -un)" ]; then
sudo chown "$(id -un):$(id -gn)" "$dir"
fi
done

CI=true vp i
# Repairs electron's path.txt and exec bits after install, same as CI.
vp run --filter @t3tools/desktop ensure:electron
# Pre-warms Vite's dep optimizer (cache is keyed on the absolute path, which
# is stable inside the container).
node apps/web/scripts/warm-dep-cache.ts
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.38",
"version": "0.0.39",
"private": true,
"type": "module",
"main": "dist-electron/main.cjs",
Expand Down
116 changes: 15 additions & 101 deletions apps/marketing/src/lib/homeMotion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,8 @@ class ElementStub extends EventTarget {
properties = new Map<string, string>();
style = { setProperty: (name: string, value: string) => this.properties.set(name, value) };
children: ElementStub[] = [];
scrollLeft = 0;
scrollWidth = 1_200;
clientWidth = 400;
matches = () => false;
contains = (target: EventTarget | null) =>
target === this || (target instanceof ElementStub && this.children.includes(target));
querySelectorAll = () => this.children;
getBoundingClientRect = vi.fn(() => ({ left: 0, top: 0, width: 400, height: 600 }));
scrollTo = vi.fn((options: ScrollToOptions) => {
this.scrollLeft = options.left ?? this.scrollLeft;
});
}

let observers: ObserverStub[] = [];
Expand All @@ -34,29 +25,25 @@ class ObserverStub {
}
}

let page = Object.assign(new EventTarget(), { visibilityState: "visible", activeElement: null });
let viewport = new EventTarget();
let page = Object.assign(new EventTarget(), { visibilityState: "visible" });
let reduced = Object.assign(new EventTarget(), { matches: false });
let fine = Object.assign(new EventTarget(), { matches: true });
let frames = new Map<number, FrameRequestCallback>();
let dispose: (() => void) | undefined;

beforeEach(() => {
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
observers = [];
frames = new Map();
page = Object.assign(new EventTarget(), { visibilityState: "visible", activeElement: null });
viewport = new EventTarget();
page = Object.assign(new EventTarget(), { visibilityState: "visible" });
reduced = Object.assign(new EventTarget(), { matches: false });
fine = Object.assign(new EventTarget(), { matches: true });
vi.stubGlobal("document", page);
vi.stubGlobal(
"window",
Object.assign(viewport, {
Object.assign(new EventTarget(), {
matchMedia: (query: string) => (query.includes("reduced-motion") ? reduced : fine),
}),
);
vi.stubGlobal("Node", ElementStub);
vi.stubGlobal("IntersectionObserver", ObserverStub);
let frameId = 0;
vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => {
Expand All @@ -69,7 +56,6 @@ beforeEach(() => {
afterEach(() => {
dispose?.();
dispose = undefined;
vi.useRealTimers();
vi.unstubAllGlobals();
});

Expand All @@ -79,26 +65,29 @@ function fixture() {
const mark = new ElementStub();
const otherMark = new ElementStub();
field.children = [mark, otherMark];
const endorsements = new ElementStub();
const track = new ElementStub();
const caret = new ElementStub();
dispose = startHomeMotion({ hero, field, endorsements, caret } as unknown as Parameters<
dispose = startHomeMotion({ hero, field, tracks: [track], caret } as unknown as Parameters<
typeof startHomeMotion
>[0]);
return { hero, field, mark, otherMark, endorsements, caret, observer: observers[0]! };
return { hero, field, mark, otherMark, track, caret, observer: observers[0]! };
}

function movePointer(hero: ElementStub, x = 400, y = 600) {
hero.dispatchEvent(Object.assign(new Event("pointermove"), { clientX: x, clientY: y }));
}

describe("homepage motion", () => {
it("gates each mark and caret and batches pointer input into one frame", () => {
const { hero, field, mark, otherMark, caret, observer } = fixture();
it("gates each mark, marquee track, and caret and batches pointer input into one frame", () => {
const { hero, field, mark, otherMark, track, caret, observer } = fixture();
expect(mark.properties.get("--home-motion-state")).toBe("paused");
expect(track.properties.get("--home-motion-state")).toBe("paused");
observer.report(mark, true);
observer.report(track, true);
observer.report(caret, true);
expect(mark.properties.get("--home-motion-state")).toBe("running");
expect(otherMark.properties.get("--home-motion-state")).toBe("paused");
expect(track.properties.get("--home-motion-state")).toBe("running");
expect(caret.properties.get("--home-motion-state")).toBe("running");

movePointer(hero, 100, 100);
Expand All @@ -117,6 +106,7 @@ describe("homepage motion", () => {
expect(frames.size).toBe(0);
expect(field.properties.get("--px")).toBe("0px");
expect(mark.properties.get("--home-motion-state")).toBe("paused");
expect(track.properties.get("--home-motion-state")).toBe("paused");
expect(caret.properties.get("--home-motion-state")).toBe("paused");
page.visibilityState = "visible";
page.dispatchEvent(new Event("visibilitychange"));
Expand All @@ -133,94 +123,18 @@ describe("homepage motion", () => {
expect(mark.properties.get("--home-motion-state")).toBe("running");
});

it("pages every eight seconds, reverses at the end, and has no timer without overflow", () => {
const { endorsements, observer } = fixture();
expect(vi.getTimerCount()).toBe(0);
observer.report(endorsements, true);
vi.advanceTimersByTime(7_999);
expect(endorsements.scrollTo).not.toHaveBeenCalled();
vi.advanceTimersByTime(16_001);
expect(endorsements.scrollTo.mock.calls.map(([options]) => options.left)).toEqual([
400, 800, 400,
]);
expect(
endorsements.scrollTo.mock.calls.every(([options]) => options.behavior === "smooth"),
).toBe(true);

endorsements.clientWidth = endorsements.scrollWidth;
viewport.dispatchEvent(new Event("resize"));
expect(vi.getTimerCount()).toBe(0);
expect(endorsements.scrollTo).toHaveBeenLastCalledWith({ left: 400, behavior: "instant" });
endorsements.clientWidth = 400;
viewport.dispatchEvent(new Event("resize"));
expect(vi.getTimerCount()).toBe(1);
});

it("pauses paging for hover, focus, hidden content, and reduced motion", () => {
const { endorsements, observer } = fixture();
observer.report(endorsements, true);
const changeVisibility = (visible: boolean) => {
page.visibilityState = visible ? "visible" : "hidden";
page.dispatchEvent(new Event("visibilitychange"));
};
const changeMotion = (matches: boolean) => {
reduced.matches = matches;
reduced.dispatchEvent(new Event("change"));
};
const pauses = [
[
() => endorsements.dispatchEvent(new Event("pointerenter")),
() => endorsements.dispatchEvent(new Event("pointerleave")),
],
[
() => endorsements.dispatchEvent(new Event("focusin")),
() =>
endorsements.dispatchEvent(Object.assign(new Event("focusout"), { relatedTarget: null })),
],
[() => changeVisibility(false), () => changeVisibility(true)],
[() => observer.report(endorsements, false), () => observer.report(endorsements, true)],
[() => changeMotion(true), () => changeMotion(false)],
] as const;
for (const [pause, resume] of pauses) {
pause();
expect(vi.getTimerCount()).toBe(0);
vi.advanceTimersByTime(16_000);
resume();
expect(vi.getTimerCount()).toBe(1);
}
expect(endorsements.scrollTo).not.toHaveBeenCalled();
vi.advanceTimersByTime(8_000);
expect(endorsements.scrollTo).toHaveBeenCalledWith({ left: 400, behavior: "smooth" });
endorsements.dispatchEvent(new Event("pointerenter"));
expect(endorsements.scrollTo).toHaveBeenLastCalledWith({ left: 400, behavior: "instant" });
expect(vi.getTimerCount()).toBe(0);
});

it.each(["wheel", "pointerdown", "keydown"])("hands control to the user after %s", (event) => {
const { endorsements, observer } = fixture();
observer.report(endorsements, true);
endorsements.dispatchEvent(new Event(event));
observer.report(endorsements, false);
observer.report(endorsements, true);
endorsements.dispatchEvent(new Event("pointerleave"));
viewport.dispatchEvent(new Event("resize"));
vi.advanceTimersByTime(60_000);
expect(vi.getTimerCount()).toBe(0);
expect(endorsements.scrollTo).not.toHaveBeenCalled();
});

it("cancels pending work and ignores events after cleanup", () => {
const { hero, mark, endorsements, observer } = fixture();
const { hero, mark, track, observer } = fixture();
observer.report(mark, true);
observer.report(endorsements, true);
observer.report(track, true);
movePointer(hero);
dispose?.();
observer.report(mark, true);
movePointer(hero);
reduced.dispatchEvent(new Event("change"));
expect(observer.disconnect).toHaveBeenCalledTimes(1);
expect(frames.size).toBe(0);
expect(vi.getTimerCount()).toBe(0);
expect(mark.properties.get("--home-motion-state")).toBe("paused");
expect(track.properties.get("--home-motion-state")).toBe("paused");
});
});
Loading
Loading