Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2028d57
Fix server publish check for bin entrypoint (#1885)
juliusmarminge Apr 10, 2026
a3f2927
chore(release): prepare v0.0.16
t3-code[bot] Apr 10, 2026
9385314
Persist changed-files expansion state per thread (#1858)
Marve10s Apr 10, 2026
e3004ae
Harden secret store and resolve catalog overrides (#1891)
juliusmarminge Apr 10, 2026
a3dadf3
chore(release): prepare v0.0.17
t3-code[bot] Apr 10, 2026
678f827
Remove Claude subscription-based model adjustment (#1899)
juliusmarminge Apr 10, 2026
e231681
Fix worktree base branch updates for active draft (#1900)
juliusmarminge Apr 10, 2026
12c3af7
feat(desktop): add "Copy Image" to right-click context menu (#1052)
GuilhermeVieiraDev Apr 10, 2026
5fa09fa
[codex] fix composer footer compact layout (#1894)
shivamhwp Apr 10, 2026
4ae9de3
Stabilize auth session cookies per server mode (#1898)
juliusmarminge Apr 10, 2026
58e5f71
Add provider skill discovery (#1905)
juliusmarminge Apr 11, 2026
811573c
Sync upstream and refine provider discovery
gabrielMalonso Apr 11, 2026
90c603d
Refactor composer send and placeholder helpers
gabrielMalonso Apr 11, 2026
d6b5356
Show Codex-discovered skills and commands in composer
gabrielMalonso Apr 11, 2026
4b66b6a
Tighten composer and timeline sizing
gabrielMalonso Apr 11, 2026
79327f7
Reorder composer extension controls
gabrielMalonso Apr 11, 2026
587dfac
Capture composer paste before default handling
gabrielMalonso Apr 11, 2026
485bf64
Harden secret store errors and relax composer footer budget
gabrielMalonso Apr 11, 2026
00e20e2
test(browser): estabiliza tolerancias do CI
gabrielMalonso Apr 11, 2026
4bcd3f1
test(browser): ajusta thresholds do runner linux
gabrielMalonso Apr 11, 2026
4a04f44
test(browser): reduz width do overflow no linux
gabrielMalonso Apr 11, 2026
f42ba6c
test(browser): fixa width de overflow
gabrielMalonso Apr 11, 2026
84f3490
test(browser): valida contencao do footer
gabrielMalonso Apr 11, 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
45 changes: 45 additions & 0 deletions .context/upstream-sync.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Upstream Sync

## Status atual

- Data: 2026-04-10
- Branch de trabalho: `t3code/upstream-sync-check`
- Upstream integrado nesta wave: `58e5f714b03ec44b42f00b52947a73d991fb8d8a` (`upstream/main`)
- Estado: merge aplicado, conflitos resolvidos e validacao local concluida; falta apenas commitar se quisermos fechar o merge no historico

## Features locais vivas

- `t3code-custom/file-references`: referencia de arquivos por path, colagem e envio
- `t3code-custom/chat/ThreadLoop*`: controles e comportamento de thread loop
- `t3code-custom/hooks/useComposerFileReferenceSend.ts`: serializacao custom no envio
- `t3code-custom/chat/useComposerSkillExtension.ts`: mapeia skills do Codex selecionadas no prompt para `{ name, path }` no send

## Refatoracoes feitas para sair da frente do upstream

- O composer agora usa o fluxo nativo do upstream para chips e busca de skills/slash commands
- Removido `apps/web/src/components/composerInlineTextNodes.ts`, que virou duplicacao da infraestrutura nova do upstream
- A logica custom de skill ficou reduzida ao que realmente e local: derivar `selectedSkills` para o envio do Codex
- `ChatComposer.tsx` voltou a depender de `selectedProviderStatus.skills` e `selectedProviderStatus.slashCommands`, em vez de puxar catalogo paralelo so para UI
- `ComposerPromptEditor.tsx` manteve o snapshot ampliado necessario para o paste custom de file references sem reabrir um fork inteiro do editor
- A placeholder custom do composer saiu de `ChatComposer.tsx` e voltou para `t3code-custom/chat/composerPlaceholder.ts`
- A orquestracao custom de envio do composer foi empurrada para `t3code-custom/hooks/useComposerSendExtension.ts`, reduzindo regra local espalhada em `ChatView.tsx`
- `ComposerPromptEditor.tsx` parou de persistir estado extra de selecao no snapshot interno; a leitura ampliada agora acontece so quando precisa

## Hotspots que continuam sensiveis

- `apps/web/src/components/chat/ChatComposer.tsx`
Continua sendo o ponto de encaixe entre UX do core e extensoes locais do composer
- `apps/web/src/components/ComposerPromptEditor.tsx`
Qualquer mudanca de snapshot, cursor ou selection mexe direto com paste custom e chips inline
- `apps/web/src/components/ChatView.tsx`
Ainda concentra ligacao entre envio, timeline e hooks custom, mas menos regra local ficou espalhada ali
- `apps/web/src/composerDraftStore.ts`
Permanece hotspot compartilhado para draft, imagens, terminal context e file references
- `apps/web/src/components/chat/MessagesTimeline.tsx`
Continua sendo fronteira entre renderizacao do core e parser dos sentinelas custom

## Regra pratica para o proximo sync

- Se a mudanca for UX de skill/slash command, tentar absorver do upstream primeiro
- Se a mudanca for regra de negocio local, empurrar para `t3code-custom/*`
- Se precisar tocar `ChatComposer` ou `ComposerPromptEditor`, fazer o minimo e deixar a adaptacao visivel
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.15",
"version": "0.0.17",
"private": true,
"main": "dist-electron/main.js",
"scripts": {
Expand Down
55 changes: 54 additions & 1 deletion apps/desktop/src/backendPort.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,57 @@ describe("resolveDesktopBackendPort", () => {
]);
});

it("treats wildcard-bound ports as unavailable even when loopback probing succeeds", async () => {
const canListenOnHost = vi.fn(async (port: number, host: string) => {
if (port === 3773 && host === "127.0.0.1") return true;
if (port === 3773 && host === "0.0.0.0") return false;
return port === 3774;
});

await expect(
resolveDesktopBackendPort({
host: "127.0.0.1",
requiredHosts: ["0.0.0.0"],
startPort: 3773,
canListenOnHost,
}),
).resolves.toBe(3774);

expect(canListenOnHost.mock.calls).toEqual([
[3773, "127.0.0.1"],
[3773, "0.0.0.0"],
[3774, "127.0.0.1"],
[3774, "0.0.0.0"],
]);
});

it("checks overlapping hosts sequentially to avoid self-interference", async () => {
let inFlightCount = 0;
const canListenOnHost = vi.fn(async (_port: number, _host: string) => {
inFlightCount += 1;
const overlapped = inFlightCount > 1;
await Promise.resolve();
inFlightCount -= 1;
return !overlapped;
});

await expect(
resolveDesktopBackendPort({
host: "127.0.0.1",
requiredHosts: ["0.0.0.0", "::"],
startPort: 3773,
maxPort: 3773,
canListenOnHost,
}),
).resolves.toBe(3773);

expect(canListenOnHost.mock.calls).toEqual([
[3773, "127.0.0.1"],
[3773, "0.0.0.0"],
[3773, "::"],
]);
});

it("fails when the scan range is exhausted", async () => {
const canListenOnHost = vi.fn(async () => false);

Expand All @@ -46,7 +97,9 @@ describe("resolveDesktopBackendPort", () => {
maxPort: 65535,
canListenOnHost,
}),
).rejects.toThrow("No desktop backend port is available on 127.0.0.1 between 65534 and 65535");
).rejects.toThrow(
"No desktop backend port is available on hosts 127.0.0.1 between 65534 and 65535",
);

expect(canListenOnHost.mock.calls).toEqual([
[65534, "127.0.0.1"],
Expand Down
34 changes: 32 additions & 2 deletions apps/desktop/src/backendPort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface ResolveDesktopBackendPortOptions {
readonly host: string;
readonly startPort?: number;
readonly maxPort?: number;
readonly requiredHosts?: ReadonlyArray<string>;
readonly canListenOnHost?: (port: number, host: string) => Promise<boolean>;
}

Expand All @@ -21,10 +22,37 @@ const defaultCanListenOnHost = async (port: number, host: string): Promise<boole
const isValidPort = (port: number): boolean =>
Number.isInteger(port) && port >= 1 && port <= MAX_TCP_PORT;

const normalizeHosts = (
host: string,
requiredHosts: ReadonlyArray<string>,
): ReadonlyArray<string> =>
Array.from(
new Set(
[host, ...requiredHosts]
.map((candidate) => candidate.trim())
.filter((candidate) => candidate.length > 0),
),
);

async function canListenOnAllHosts(
port: number,
hosts: ReadonlyArray<string>,
canListenOnHost: (port: number, host: string) => Promise<boolean>,
): Promise<boolean> {
for (const candidateHost of hosts) {
if (!(await canListenOnHost(port, candidateHost))) {
return false;
}
}

return true;
}

export async function resolveDesktopBackendPort({
host,
startPort = DEFAULT_DESKTOP_BACKEND_PORT,
maxPort = MAX_TCP_PORT,
requiredHosts = [],
canListenOnHost = defaultCanListenOnHost,
}: ResolveDesktopBackendPortOptions): Promise<number> {
if (!isValidPort(startPort)) {
Expand All @@ -39,15 +67,17 @@ export async function resolveDesktopBackendPort({
throw new Error(`Desktop backend max port ${maxPort} is below start port ${startPort}`);
}

const hostsToCheck = normalizeHosts(host, requiredHosts);

// Keep desktop startup predictable across app restarts by probing upward from
// the same preferred port instead of picking a fresh ephemeral port.
for (let port = startPort; port <= maxPort; port += 1) {
if (await canListenOnHost(port, host)) {
if (await canListenOnAllHosts(port, hostsToCheck, canListenOnHost)) {
return port;
}
}

throw new Error(
`No desktop backend port is available on ${host} between ${startPort} and ${maxPort}`,
`No desktop backend port is available on hosts ${hostsToCheck.join(", ")} between ${startPort} and ${maxPort}`,
);
}
9 changes: 9 additions & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1695,6 +1695,14 @@ function createWindow(): BrowserWindow {
);
}

if (params.mediaType === "image") {
menuTemplate.push({
label: "Copy Image",
click: () => window.webContents.copyImageAt(params.x, params.y),
});
menuTemplate.push({ type: "separator" });
}

menuTemplate.push(
{ role: "cut", enabled: params.editFlags.canCut },
{ role: "copy", enabled: params.editFlags.canCopy },
Expand Down Expand Up @@ -1773,6 +1781,7 @@ async function bootstrap(): Promise<void> {
(await resolveDesktopBackendPort({
host: DESKTOP_LOOPBACK_HOST,
startPort: DEFAULT_DESKTOP_BACKEND_PORT,
requiredHosts: desktopSettings.serverExposureMode === "network-accessible" ? ["0.0.0.0"] : [],
}));
writeDesktopLogHeader(
configuredBackendPort === undefined
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.0.15",
"version": "0.0.17",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
28 changes: 25 additions & 3 deletions apps/server/scripts/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog
import rootPackageJson from "../../../package.json" with { type: "json" };
import serverPackageJson from "../package.json" with { type: "json" };

interface PackageJson {
name: string;
repository: {
type: string;
url: string;
directory: string;
};
bin: Record<string, string>;
type: string;
version: string;
engines: Record<string, string>;
files: string[];
dependencies: Record<string, string>;
overrides: Record<string, string>;
}

class CliError extends Data.TaggedError("CliError")<{
readonly message: string;
readonly cause?: unknown;
Expand Down Expand Up @@ -177,7 +193,7 @@ const publishCmd = Command.make(
const backupPath = `${packageJsonPath}.bak`;

// Assert build assets exist
for (const relPath of ["dist/index.mjs", "dist/client/index.html"]) {
for (const relPath of ["dist/bin.mjs", "dist/client/index.html"]) {
const abs = path.join(serverDir, relPath);
if (!(yield* fs.exists(abs))) {
return yield* new CliError({
Expand All @@ -192,22 +208,28 @@ const publishCmd = Command.make(
// Resolve catalog dependencies before any file mutations. If this throws,
// acquire fails and no release hook runs, so filesystem must still be untouched.
const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);
const pkg = {
const pkg: PackageJson = {
name: serverPackageJson.name,
repository: serverPackageJson.repository,
bin: serverPackageJson.bin,
type: serverPackageJson.type,
version,
engines: serverPackageJson.engines,
files: serverPackageJson.files,
dependencies: serverPackageJson.dependencies as Record<string, unknown>,
dependencies: serverPackageJson.dependencies,
overrides: rootPackageJson.overrides,
};

pkg.dependencies = resolveCatalogDependencies(
pkg.dependencies,
rootPackageJson.workspaces.catalog,
"apps/server dependencies",
);
pkg.overrides = resolveCatalogDependencies(
pkg.overrides,
rootPackageJson.workspaces.catalog,
"root overrides",
);

const original = yield* fs.readFileString(packageJsonPath);
yield* fs.writeFileString(backupPath, original);
Expand Down
12 changes: 11 additions & 1 deletion apps/server/src/auth/Layers/ServerAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ import {
AuthError,
type ServerAuthShape,
} from "../Services/ServerAuth.ts";
import { SessionCredentialService } from "../Services/SessionCredentialService.ts";
import {
SessionCredentialError,
SessionCredentialService,
} from "../Services/SessionCredentialService.ts";
import { AuthControlPlaneLive, AuthCoreLive } from "./AuthControlPlane.ts";

type BootstrapExchangeResult = {
Expand Down Expand Up @@ -65,6 +68,13 @@ export const makeServerAuth = Effect.gen(function* () {

const authenticateToken = (token: string): Effect.Effect<AuthenticatedSession, AuthError> =>
sessions.verify(token).pipe(
Effect.tapError((cause: SessionCredentialError) =>
Effect.logWarning("Rejected authenticated session credential.").pipe(
Effect.annotateLogs({
reason: cause.message,
}),
),
),
Effect.map((session) => ({
sessionId: session.sessionId,
subject: session.subject,
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/auth/Layers/ServerAuthPolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,12 @@ it.layer(NodeServices.layer)("ServerAuthPolicyLive", (it) => {

expect(descriptor.policy).toBe("desktop-managed-local");
expect(descriptor.bootstrapMethods).toEqual(["desktop-bootstrap"]);
expect(descriptor.sessionCookieName).toBe("t3_session_3773");
}).pipe(
Effect.provide(
makeServerAuthPolicyLayer({
mode: "desktop",
port: 3773,
}),
),
),
Expand Down Expand Up @@ -66,6 +68,7 @@ it.layer(NodeServices.layer)("ServerAuthPolicyLive", (it) => {

expect(descriptor.policy).toBe("loopback-browser");
expect(descriptor.bootstrapMethods).toEqual(["one-time-token"]);
expect(descriptor.sessionCookieName).toBe("t3_session");
}).pipe(
Effect.provide(
makeServerAuthPolicyLayer({
Expand Down
7 changes: 5 additions & 2 deletions apps/server/src/auth/Layers/ServerAuthPolicy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Effect, Layer } from "effect";

import { ServerConfig } from "../../config.ts";
import { ServerAuthPolicy, type ServerAuthPolicyShape } from "../Services/ServerAuthPolicy.ts";
import { SESSION_COOKIE_NAME } from "../utils.ts";
import { resolveSessionCookieName } from "../utils.ts";
import { isLoopbackHost, isWildcardHost } from "../../startupAccess.ts";

export const makeServerAuthPolicy = Effect.gen(function* () {
Expand All @@ -30,7 +30,10 @@ export const makeServerAuthPolicy = Effect.gen(function* () {
policy,
bootstrapMethods,
sessionMethods: ["browser-session-cookie", "bearer-session-token"],
sessionCookieName: SESSION_COOKIE_NAME,
sessionCookieName: resolveSessionCookieName({
mode: config.mode,
port: config.port,
}),
};

return {
Expand Down
Loading
Loading