Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
31 changes: 29 additions & 2 deletions .github/workflows/windows-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ permissions:
jobs:
test:
name: Test (${{ inputs.package || 'all non-server' }})
runs-on: blacksmith-8vcpu-windows-2025
# Forks do not inherit upstream's Blacksmith runner integration.
runs-on: ${{ github.repository == 'pingdotgg/t3code' && 'blacksmith-8vcpu-windows-2025' || 'windows-2025' }}
timeout-minutes: 45
steps:
- name: Checkout
Expand Down Expand Up @@ -64,6 +65,18 @@ jobs:
if: inputs.package == '' || inputs.package == 'apps/desktop'
run: vp run --filter "@t3tools/desktop" ensure:electron

- name: Install Chromium
if: inputs.package == '' || inputs.package == 'apps/web'
run: node apps/web/node_modules/playwright/cli.js install --only-shell chromium

- name: Install server Chromium
if: inputs.package == 'apps/server'
run: node apps/server/node_modules/playwright-core/cli.js install --only-shell chromium

- name: Build server native addon
if: inputs.package == 'apps/server' && inputs.files != ''
run: node native/project-memory/build.mjs

# `vp run ... test -- <files>` does not forward positional args to vitest,
# so file-scoped runs call `vp test run` inside the package instead.
- name: Test
Expand All @@ -72,7 +85,21 @@ jobs:
$package = '${{ inputs.package }}'
$files = '${{ inputs.files }}'
if ($package -eq '') {
vp run --parallel --concurrency-limit 4 --filter '!t3' --filter '!@t3tools/monorepo' test
$packages = node -e "const { readWorkspace } = require('./.github/scripts/ci-changes.cjs'); for (const pkg of readWorkspace()) if (pkg.hasTest && pkg.name !== 't3' && pkg.name !== '@t3tools/monorepo') console.log(pkg.dir)"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
$failed = @()
foreach ($workspace in $packages) {
Write-Output "::group::Test $workspace"
vp run --filter "./$workspace" test
$exitCode = $LASTEXITCODE
Write-Output "::endgroup::"
if ($exitCode -ne 0) { $failed += $workspace }
}
if ($failed.Count -gt 0) {
Write-Output "::error::Failed test suites: $($failed -join ', ')"
exit 1
}
exit 0
} elseif ($files -eq '') {
vp run --filter "./$package" test
} else {
Expand Down
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.1.5",
"version": "0.1.6",
"private": true,
"type": "module",
"main": "dist-electron/boot.cjs",
Expand Down
8 changes: 1 addition & 7 deletions apps/mobile/src/features/threads/ThreadRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ import { useSelectedThreadWorktree } from "../../state/use-selected-thread-workt
import { useThreadComposerState } from "../../state/use-thread-composer-state";
import { resolveMergeBackTargetThreadId } from "@t3tools/client-runtime/state/thread-relationships";
import { resolveLatestMergeBackRun } from "@t3tools/client-runtime/state/thread-workflows";
import { threadEnvironment, useThreadFamily } from "../../state/threads";
import { threadEnvironment } from "../../state/threads";
import { EMPTY_ENVIRONMENT_THREAD_STATE } from "@t3tools/client-runtime/state/threads";
import { useProjects } from "../../state/entities";
import { projectThreadContentPresentation } from "./threadContentPresentation";
Expand Down Expand Up @@ -244,12 +244,6 @@ function LiveThreadRouteController(props: ThreadRouteScreenProps) {
const connectionsReady = useConnectionsReady();
const { connectionState } = useRemoteConnectionStatus();
const { selectedThread } = useThreadSelection();
// Subagent rows, the open child's own row and its ancestors come from the family when the
// shell leaves idle children out.
useThreadFamily(
selectedThread?.environmentId ?? null,
selectedThread?.lineage.rootThreadId ?? null,
);
const params = props.route.params ?? {};
const environmentIdRaw = firstRouteParam(params.environmentId);
const threadIdRaw = firstRouteParam(params.threadId);
Expand Down
13 changes: 0 additions & 13 deletions apps/mobile/src/state/threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import {
} from "@t3tools/client-runtime/state/threads";
import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
import { AsyncResult, Atom } from "effect/reactivity";
import { useEffect } from "react";

import { environmentCatalog } from "../connection/catalog";
import { connectionAtomRuntime } from "../connection/runtime";
Expand Down Expand Up @@ -45,18 +44,6 @@ export const environmentThreadShells = createEnvironmentThreadShellAtoms({
snapshotAtom: threadEnvironment.snapshotAtom,
});

/** Keeps the subagent tree under `rootThreadId` in the thread list while the caller shows it. */
export function useThreadFamily(
environmentId: EnvironmentId | null,
rootThreadId: ThreadId | null,
): void {
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- mounts the subagent tree under the root thread into the shared thread registry while the caller shows it
useEffect(() => {
if (environmentId === null || rootThreadId === null) return;
return threadFamilies.mount(appAtomRegistry, environmentId, rootThreadId);
}, [environmentId, rootThreadId]);
}

// Long enough for the opened screen to subscribe; its subscription keeps the thread after this.
const OPENING_THREAD_HOLD_MS = 3_000;

Expand Down
2 changes: 1 addition & 1 deletion apps/server/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "t3",
"version": "0.1.5",
"version": "0.1.6",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion apps/server/src/device/deviceToolVersions.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"agent-device": "0.21.31",
"expo-device-hub": "0.19.0-canary-20261023-1357114"
"expo-device-hub": "0.19.1"
}
23 changes: 23 additions & 0 deletions apps/server/src/provider/antigravityUsageLimits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ const fixture = Effect.fn("AntigravityUsageTest.fixture")(function* (
);
}
expect(JSON.parse(body)).toEqual({ project: "project-id" });
expect(request.headers["user-agent"]).toMatch(/^antigravity\/hub\/2\.24\.0 /);
return HttpClientResponse.fromWeb(
request,
request.url.endsWith(":retrieveUserQuotaSummary")
Expand Down Expand Up @@ -475,6 +476,28 @@ describe("Antigravity subscription limits", () => {
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("labels Argon's weekly quota apart from the default Gemini weekly quota", () =>
Effect.gen(function* () {
const { probe } = yield* fixture({
summary: {
groups: [
...summary.groups,
{
displayName: "Gemini 4 Argon",
buckets: [
{ bucketId: "gemini-argon-wtu-weekly", window: "weekly", remainingFraction: 0.6 },
],
},
],
},
});
const limits = yield* probe;
const byId = new Map(limits.windows.map((window) => [window.id, window]));
expect(byId.get("gemini-weekly")?.label).toBe("Gemini · Weekly");
expect(byId.get("gemini-argon-wtu-weekly")?.label).toBe("Gemini 4 Argon · Weekly");
}).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
);

it.effect("keeps one endpoint's quotas when the other fails", () =>
Effect.gen(function* () {
const { probe } = yield* fixture({
Expand Down
15 changes: 9 additions & 6 deletions apps/server/src/provider/antigravityUsageLimits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,14 @@ function summaryWindows(summary: typeof Summary.Type, requestedAt: number) {
for (const group of groups ?? []) {
for (const bucket of group.buckets ?? []) {
if (bucket.disabled) continue;
const family = bucket.bucketId.startsWith("gemini-")
? "Gemini"
: bucket.bucketId.startsWith("3p-")
? "Claude & GPT (shared)"
: group.displayName?.trim() || bucket.bucketId;
// Argon is metered apart from the default Gemini buckets; keep its group name.
const family = bucket.bucketId.startsWith("gemini-argon-")
? group.displayName?.trim() || "Gemini Argon"
: bucket.bucketId.startsWith("gemini-")
? "Gemini"
: bucket.bucketId.startsWith("3p-")
? "Claude & GPT (shared)"
: group.displayName?.trim() || bucket.bucketId;
const weekly = bucket.window === "weekly";
const session = bucket.window === "5h";
const windowDurationMins = SUMMARY_WINDOW_MINS.get(bucket.window ?? "");
Expand Down Expand Up @@ -284,7 +287,7 @@ export const makeAntigravityUsageProbe = Effect.fn("makeAntigravityUsageProbe")(
HttpClientRequest.post(`${endpoint}/v1internal:${operation}`).pipe(
HttpClientRequest.setHeaders({
Authorization: `Bearer ${accessToken}`,
"User-Agent": `antigravity/hub/1.0.0 (aidev_client; os_type=${platform}; arch=${architecture}; cl=963137146)`,
"User-Agent": `antigravity/hub/2.24.0 (aidev_client; os_type=${platform}; arch=${architecture}; cl=963137146)`,
}),
HttpClientRequest.bodyJsonUnsafe(
token.project_id ? { project: token.project_id } : {},
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@t3tools/web",
"version": "0.1.5",
"version": "0.1.6",
"private": true,
"type": "module",
"scripts": {
Expand Down
14 changes: 4 additions & 10 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ import { isEditableFocused } from "../lib/editableFocus";
import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings";
import { shortcutLabelForCommand } from "../keybindings";
import { cn } from "~/lib/utils";
import { isModelPickerOpen } from "../modelPickerVisibility";
import { COLLAPSED_SIDEBAR_TITLEBAR_INSET_CLASS } from "~/workspaceTitlebar";
import { toastManager } from "./ui/toast";
import { projectScriptIdFromCommand } from "~/projectScripts";
Expand Down Expand Up @@ -234,7 +235,7 @@ import {
primaryServerKeybindingsAtom,
serverEnvironment,
} from "../state/server";
import { threadEnvironment, useThreadFamily } from "../state/threads";
import { threadEnvironment } from "../state/threads";
import { resolveProviderSkillsForCwd } from "@t3tools/client-runtime/providerSkills";
import { vcsEnvironment } from "../state/vcs";
import {
Expand Down Expand Up @@ -488,14 +489,6 @@ export default function ChatView(props: ChatViewProps) {
});
const serverThreadProjection = useQuietThreadProjection(routeThreadDetailRef);
const serverProjection = serverThreadProjection?.projection ?? null;
// Subagent rows, an open child's own row and its ancestors come from the family when the shell
// leaves idle children out. Every source agrees on the root, so the key never flips.
useThreadFamily(
routeKind === "server" ? environmentId : null,
serverThreadShell?.lineage.rootThreadId ??
serverProjection?.thread.lineage.rootThreadId ??
null,
);
// Only the shell-less fallback needs these; with a shell, the shell carries them.
const fallbackDetailRef = serverThreadShell === null ? routeThreadDetailRef : null;
const fallbackUpdatedAt = useThreadUpdatedAt(fallbackDetailRef);
Expand Down Expand Up @@ -1858,7 +1851,8 @@ export default function ChatView(props: ChatViewProps) {
);
const scheduleComposerFocus = () => {
window.requestAnimationFrame(() => {
focusComposer();
// The model picker stays open through its own changes, so it keeps focus.
if (!isModelPickerOpen()) focusComposer();
});
};
const scheduleComposerFocusStable = useStableCallback(scheduleComposerFocus);
Expand Down
11 changes: 6 additions & 5 deletions apps/web/src/components/chat/ComposerControlsBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,8 @@ import { resolveComposerControlsOwnership } from "./composerControlsOwnership";
* permissions), mode controls and the overflow menu for blocks that do not fit. Rendered in the
* expanded footer or the resting controls host.
*/
// Model and effort share one trigger; hover and open states brighten its text, not a box.
const TEXT_CONTROL_STATES =
"hover:bg-transparent data-pressed:bg-transparent data-popup-open:bg-transparent data-pressed:text-foreground data-popup-open:text-foreground";
// Model and effort share one trigger; it stays filled while its popup is open.
const MODEL_TRIGGER_STATES = "data-popup-open:bg-accent data-popup-open:text-foreground";

export function ComposerControlsBar(props: {
environmentId: EnvironmentId;
Expand Down Expand Up @@ -196,6 +195,8 @@ export function ComposerControlsBar(props: {
environmentId={environmentId}
compact={false}
isComposerOwned
// The trigger grows away from this edge when its model or effort label changes.
popupAlign={composerControlsCollapsed ? "start" : "end"}
disabled={providerCatalogPending || isSendBusy}
multipleModelsEnabled={multipleModelsEnabled}
multipleModelSelections={multipleModelSelections}
Expand All @@ -220,8 +221,8 @@ export function ComposerControlsBar(props: {
modelOptionsByInstance={modelOptionsByInstance}
size={controlSize}
triggerClassName={cn(
"group/model-trigger pe-1",
TEXT_CONTROL_STATES,
"group/model-trigger",
MODEL_TRIGGER_STATES,
composerControlsCollapsed ? "min-w-13 shrink text-xs!" : "min-w-13",
)}
triggerSuffix={effort.suffix}
Expand Down
35 changes: 13 additions & 22 deletions apps/web/src/components/chat/ComposerModelEffort.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ type ColumnControl =

/**
* The column lists the model's reasoning control first, under the header that names it, then
* its other options. Picking a level of that control finishes the job and closes the popup.
* its other options.
*/
function columnControls(
provider: TraitsSelectionInput["provider"],
Expand Down Expand Up @@ -171,8 +171,14 @@ function ColumnRow(props: {
const content = (
<>
<span className="min-w-0 flex-1 truncate">{props.label}</span>
{props.isDefault ? <span className="text-muted-foreground/70 text-xs">Default</span> : null}
<CheckIcon aria-hidden="true" className={cn("size-3.5", !props.selected && "invisible")} />
{props.selected ? (
<>
{props.isDefault ? <span className="sr-only">Default</span> : null}
<CheckIcon aria-hidden="true" className="size-3.5" />
</>
) : props.isDefault ? (
<span className="text-muted-foreground/70 text-xs">Default</span>
) : null}
</>
);
if (!props.description) return <button {...rowProps}>{content}</button>;
Expand Down Expand Up @@ -201,7 +207,7 @@ function moveBetweenRows(event: KeyboardEvent<HTMLElement>) {
next.scrollIntoView({ block: "nearest" });
}

function EffortLevels(props: { traits: TraitsSelectionInput; onLevelPick: () => void }) {
function EffortLevels(props: { traits: TraitsSelectionInput }) {
const traits = useTraitsSelection(props.traits);
const { effort, others } = columnControls(
props.traits.provider,
Expand Down Expand Up @@ -242,10 +248,7 @@ function EffortLevels(props: { traits: TraitsSelectionInput; onLevelPick: () =>
label={value ? "On" : "Off"}
selected={(control.descriptor.currentValue === true) === value}
level={isEffort}
onPick={() => {
traits.changeBoolean(descriptor.id, value);
if (isEffort) props.onLevelPick();
}}
onPick={() => traits.changeBoolean(descriptor.id, value)}
/>
))}
</div>
Expand Down Expand Up @@ -276,10 +279,7 @@ function EffortLevels(props: { traits: TraitsSelectionInput; onLevelPick: () =>
isDefault={option.isDefault}
disabled={locked}
level={isEffort}
onPick={() => {
traits.changeSelect(control.descriptor, option.id);
if (isEffort) props.onLevelPick();
}}
onPick={() => traits.changeSelect(control.descriptor, option.id)}
/>
))}
</div>
Expand Down Expand Up @@ -327,11 +327,7 @@ export function ComposerEffortColumn(props: { traits: TraitsSelectionInput | nul
className="flex min-h-0 flex-1 flex-col gap-0.5 overflow-y-auto overscroll-contain px-2 pt-2.5 pb-1.5"
onKeyDown={moveBetweenRows}
>
{props.traits ? (
<EffortLevels traits={props.traits} onLevelPick={picker.close} />
) : (
<NoEffortRow />
)}
{props.traits ? <EffortLevels traits={props.traits} /> : <NoEffortRow />}
</div>
</>
);
Expand Down Expand Up @@ -415,17 +411,12 @@ function PermissionsMenu(props: {
options: ReadonlyArray<RuntimeModeOption>;
onChange: (mode: RuntimeMode) => void;
}) {
const picker = use(DockedPickerContext);
const triggerRef = useRef<HTMLButtonElement>(null);
const current = props.options.find((option) => option.mode === props.runtimeMode);
if (!current) return null;
const CurrentIcon = current.icon;
return (
<Menu
// Any pick, even the current mode, ends the job; a change also hands focus to the prompt.
onOpenChange={(open, details) => {
if (!open && details.reason === "item-press") picker.close();
}}
onOpenChangeComplete={(open) => {
if (open) focusCheckedPermission(triggerRef.current);
}}
Expand Down
Loading
Loading