Skip to content

Commit b291f72

Browse files
Merge pull request #39 from gabrielMalonso/t3code/upstream-sync-check
Sincroniza upstream e refina descoberta de provedores
2 parents 8ead1af + 84f3490 commit b291f72

File tree

85 files changed

+3309
-1475
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

85 files changed

+3309
-1475
lines changed

.context/upstream-sync.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Upstream Sync
2+
3+
## Status atual
4+
5+
- Data: 2026-04-10
6+
- Branch de trabalho: `t3code/upstream-sync-check`
7+
- Upstream integrado nesta wave: `58e5f714b03ec44b42f00b52947a73d991fb8d8a` (`upstream/main`)
8+
- Estado: merge aplicado, conflitos resolvidos e validacao local concluida; falta apenas commitar se quisermos fechar o merge no historico
9+
10+
## Features locais vivas
11+
12+
- `t3code-custom/file-references`: referencia de arquivos por path, colagem e envio
13+
- `t3code-custom/chat/ThreadLoop*`: controles e comportamento de thread loop
14+
- `t3code-custom/hooks/useComposerFileReferenceSend.ts`: serializacao custom no envio
15+
- `t3code-custom/chat/useComposerSkillExtension.ts`: mapeia skills do Codex selecionadas no prompt para `{ name, path }` no send
16+
17+
## Refatoracoes feitas para sair da frente do upstream
18+
19+
- O composer agora usa o fluxo nativo do upstream para chips e busca de skills/slash commands
20+
- Removido `apps/web/src/components/composerInlineTextNodes.ts`, que virou duplicacao da infraestrutura nova do upstream
21+
- A logica custom de skill ficou reduzida ao que realmente e local: derivar `selectedSkills` para o envio do Codex
22+
- `ChatComposer.tsx` voltou a depender de `selectedProviderStatus.skills` e `selectedProviderStatus.slashCommands`, em vez de puxar catalogo paralelo so para UI
23+
- `ComposerPromptEditor.tsx` manteve o snapshot ampliado necessario para o paste custom de file references sem reabrir um fork inteiro do editor
24+
- A placeholder custom do composer saiu de `ChatComposer.tsx` e voltou para `t3code-custom/chat/composerPlaceholder.ts`
25+
- A orquestracao custom de envio do composer foi empurrada para `t3code-custom/hooks/useComposerSendExtension.ts`, reduzindo regra local espalhada em `ChatView.tsx`
26+
- `ComposerPromptEditor.tsx` parou de persistir estado extra de selecao no snapshot interno; a leitura ampliada agora acontece so quando precisa
27+
28+
## Hotspots que continuam sensiveis
29+
30+
- `apps/web/src/components/chat/ChatComposer.tsx`
31+
Continua sendo o ponto de encaixe entre UX do core e extensoes locais do composer
32+
- `apps/web/src/components/ComposerPromptEditor.tsx`
33+
Qualquer mudanca de snapshot, cursor ou selection mexe direto com paste custom e chips inline
34+
- `apps/web/src/components/ChatView.tsx`
35+
Ainda concentra ligacao entre envio, timeline e hooks custom, mas menos regra local ficou espalhada ali
36+
- `apps/web/src/composerDraftStore.ts`
37+
Permanece hotspot compartilhado para draft, imagens, terminal context e file references
38+
- `apps/web/src/components/chat/MessagesTimeline.tsx`
39+
Continua sendo fronteira entre renderizacao do core e parser dos sentinelas custom
40+
41+
## Regra pratica para o proximo sync
42+
43+
- Se a mudanca for UX de skill/slash command, tentar absorver do upstream primeiro
44+
- Se a mudanca for regra de negocio local, empurrar para `t3code-custom/*`
45+
- Se precisar tocar `ChatComposer` ou `ComposerPromptEditor`, fazer o minimo e deixar a adaptacao visivel

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@t3tools/desktop",
3-
"version": "0.0.15",
3+
"version": "0.0.17",
44
"private": true,
55
"main": "dist-electron/main.js",
66
"scripts": {

apps/desktop/src/backendPort.test.ts

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,57 @@ describe("resolveDesktopBackendPort", () => {
3636
]);
3737
});
3838

39+
it("treats wildcard-bound ports as unavailable even when loopback probing succeeds", async () => {
40+
const canListenOnHost = vi.fn(async (port: number, host: string) => {
41+
if (port === 3773 && host === "127.0.0.1") return true;
42+
if (port === 3773 && host === "0.0.0.0") return false;
43+
return port === 3774;
44+
});
45+
46+
await expect(
47+
resolveDesktopBackendPort({
48+
host: "127.0.0.1",
49+
requiredHosts: ["0.0.0.0"],
50+
startPort: 3773,
51+
canListenOnHost,
52+
}),
53+
).resolves.toBe(3774);
54+
55+
expect(canListenOnHost.mock.calls).toEqual([
56+
[3773, "127.0.0.1"],
57+
[3773, "0.0.0.0"],
58+
[3774, "127.0.0.1"],
59+
[3774, "0.0.0.0"],
60+
]);
61+
});
62+
63+
it("checks overlapping hosts sequentially to avoid self-interference", async () => {
64+
let inFlightCount = 0;
65+
const canListenOnHost = vi.fn(async (_port: number, _host: string) => {
66+
inFlightCount += 1;
67+
const overlapped = inFlightCount > 1;
68+
await Promise.resolve();
69+
inFlightCount -= 1;
70+
return !overlapped;
71+
});
72+
73+
await expect(
74+
resolveDesktopBackendPort({
75+
host: "127.0.0.1",
76+
requiredHosts: ["0.0.0.0", "::"],
77+
startPort: 3773,
78+
maxPort: 3773,
79+
canListenOnHost,
80+
}),
81+
).resolves.toBe(3773);
82+
83+
expect(canListenOnHost.mock.calls).toEqual([
84+
[3773, "127.0.0.1"],
85+
[3773, "0.0.0.0"],
86+
[3773, "::"],
87+
]);
88+
});
89+
3990
it("fails when the scan range is exhausted", async () => {
4091
const canListenOnHost = vi.fn(async () => false);
4192

@@ -46,7 +97,9 @@ describe("resolveDesktopBackendPort", () => {
4697
maxPort: 65535,
4798
canListenOnHost,
4899
}),
49-
).rejects.toThrow("No desktop backend port is available on 127.0.0.1 between 65534 and 65535");
100+
).rejects.toThrow(
101+
"No desktop backend port is available on hosts 127.0.0.1 between 65534 and 65535",
102+
);
50103

51104
expect(canListenOnHost.mock.calls).toEqual([
52105
[65534, "127.0.0.1"],

apps/desktop/src/backendPort.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export interface ResolveDesktopBackendPortOptions {
88
readonly host: string;
99
readonly startPort?: number;
1010
readonly maxPort?: number;
11+
readonly requiredHosts?: ReadonlyArray<string>;
1112
readonly canListenOnHost?: (port: number, host: string) => Promise<boolean>;
1213
}
1314

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

25+
const normalizeHosts = (
26+
host: string,
27+
requiredHosts: ReadonlyArray<string>,
28+
): ReadonlyArray<string> =>
29+
Array.from(
30+
new Set(
31+
[host, ...requiredHosts]
32+
.map((candidate) => candidate.trim())
33+
.filter((candidate) => candidate.length > 0),
34+
),
35+
);
36+
37+
async function canListenOnAllHosts(
38+
port: number,
39+
hosts: ReadonlyArray<string>,
40+
canListenOnHost: (port: number, host: string) => Promise<boolean>,
41+
): Promise<boolean> {
42+
for (const candidateHost of hosts) {
43+
if (!(await canListenOnHost(port, candidateHost))) {
44+
return false;
45+
}
46+
}
47+
48+
return true;
49+
}
50+
2451
export async function resolveDesktopBackendPort({
2552
host,
2653
startPort = DEFAULT_DESKTOP_BACKEND_PORT,
2754
maxPort = MAX_TCP_PORT,
55+
requiredHosts = [],
2856
canListenOnHost = defaultCanListenOnHost,
2957
}: ResolveDesktopBackendPortOptions): Promise<number> {
3058
if (!isValidPort(startPort)) {
@@ -39,15 +67,17 @@ export async function resolveDesktopBackendPort({
3967
throw new Error(`Desktop backend max port ${maxPort} is below start port ${startPort}`);
4068
}
4169

70+
const hostsToCheck = normalizeHosts(host, requiredHosts);
71+
4272
// Keep desktop startup predictable across app restarts by probing upward from
4373
// the same preferred port instead of picking a fresh ephemeral port.
4474
for (let port = startPort; port <= maxPort; port += 1) {
45-
if (await canListenOnHost(port, host)) {
75+
if (await canListenOnAllHosts(port, hostsToCheck, canListenOnHost)) {
4676
return port;
4777
}
4878
}
4979

5080
throw new Error(
51-
`No desktop backend port is available on ${host} between ${startPort} and ${maxPort}`,
81+
`No desktop backend port is available on hosts ${hostsToCheck.join(", ")} between ${startPort} and ${maxPort}`,
5282
);
5383
}

apps/desktop/src/main.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1695,6 +1695,14 @@ function createWindow(): BrowserWindow {
16951695
);
16961696
}
16971697

1698+
if (params.mediaType === "image") {
1699+
menuTemplate.push({
1700+
label: "Copy Image",
1701+
click: () => window.webContents.copyImageAt(params.x, params.y),
1702+
});
1703+
menuTemplate.push({ type: "separator" });
1704+
}
1705+
16981706
menuTemplate.push(
16991707
{ role: "cut", enabled: params.editFlags.canCut },
17001708
{ role: "copy", enabled: params.editFlags.canCopy },
@@ -1773,6 +1781,7 @@ async function bootstrap(): Promise<void> {
17731781
(await resolveDesktopBackendPort({
17741782
host: DESKTOP_LOOPBACK_HOST,
17751783
startPort: DEFAULT_DESKTOP_BACKEND_PORT,
1784+
requiredHosts: desktopSettings.serverExposureMode === "network-accessible" ? ["0.0.0.0"] : [],
17761785
}));
17771786
writeDesktopLogHeader(
17781787
configuredBackendPort === undefined

apps/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "t3",
3-
"version": "0.0.15",
3+
"version": "0.0.17",
44
"license": "MIT",
55
"repository": {
66
"type": "git",

apps/server/scripts/cli.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,22 @@ import { resolveCatalogDependencies } from "../../../scripts/lib/resolve-catalog
1414
import rootPackageJson from "../../../package.json" with { type: "json" };
1515
import serverPackageJson from "../package.json" with { type: "json" };
1616

17+
interface PackageJson {
18+
name: string;
19+
repository: {
20+
type: string;
21+
url: string;
22+
directory: string;
23+
};
24+
bin: Record<string, string>;
25+
type: string;
26+
version: string;
27+
engines: Record<string, string>;
28+
files: string[];
29+
dependencies: Record<string, string>;
30+
overrides: Record<string, string>;
31+
}
32+
1733
class CliError extends Data.TaggedError("CliError")<{
1834
readonly message: string;
1935
readonly cause?: unknown;
@@ -177,7 +193,7 @@ const publishCmd = Command.make(
177193
const backupPath = `${packageJsonPath}.bak`;
178194

179195
// Assert build assets exist
180-
for (const relPath of ["dist/index.mjs", "dist/client/index.html"]) {
196+
for (const relPath of ["dist/bin.mjs", "dist/client/index.html"]) {
181197
const abs = path.join(serverDir, relPath);
182198
if (!(yield* fs.exists(abs))) {
183199
return yield* new CliError({
@@ -192,22 +208,28 @@ const publishCmd = Command.make(
192208
// Resolve catalog dependencies before any file mutations. If this throws,
193209
// acquire fails and no release hook runs, so filesystem must still be untouched.
194210
const version = Option.getOrElse(config.appVersion, () => serverPackageJson.version);
195-
const pkg = {
211+
const pkg: PackageJson = {
196212
name: serverPackageJson.name,
197213
repository: serverPackageJson.repository,
198214
bin: serverPackageJson.bin,
199215
type: serverPackageJson.type,
200216
version,
201217
engines: serverPackageJson.engines,
202218
files: serverPackageJson.files,
203-
dependencies: serverPackageJson.dependencies as Record<string, unknown>,
219+
dependencies: serverPackageJson.dependencies,
220+
overrides: rootPackageJson.overrides,
204221
};
205222

206223
pkg.dependencies = resolveCatalogDependencies(
207224
pkg.dependencies,
208225
rootPackageJson.workspaces.catalog,
209226
"apps/server dependencies",
210227
);
228+
pkg.overrides = resolveCatalogDependencies(
229+
pkg.overrides,
230+
rootPackageJson.workspaces.catalog,
231+
"root overrides",
232+
);
211233

212234
const original = yield* fs.readFileString(packageJsonPath);
213235
yield* fs.writeFileString(backupPath, original);

apps/server/src/auth/Layers/ServerAuth.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ import {
2020
AuthError,
2121
type ServerAuthShape,
2222
} from "../Services/ServerAuth.ts";
23-
import { SessionCredentialService } from "../Services/SessionCredentialService.ts";
23+
import {
24+
SessionCredentialError,
25+
SessionCredentialService,
26+
} from "../Services/SessionCredentialService.ts";
2427
import { AuthControlPlaneLive, AuthCoreLive } from "./AuthControlPlane.ts";
2528

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

6669
const authenticateToken = (token: string): Effect.Effect<AuthenticatedSession, AuthError> =>
6770
sessions.verify(token).pipe(
71+
Effect.tapError((cause: SessionCredentialError) =>
72+
Effect.logWarning("Rejected authenticated session credential.").pipe(
73+
Effect.annotateLogs({
74+
reason: cause.message,
75+
}),
76+
),
77+
),
6878
Effect.map((session) => ({
6979
sessionId: session.sessionId,
7080
subject: session.subject,

apps/server/src/auth/Layers/ServerAuthPolicy.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,12 @@ it.layer(NodeServices.layer)("ServerAuthPolicyLive", (it) => {
3333

3434
expect(descriptor.policy).toBe("desktop-managed-local");
3535
expect(descriptor.bootstrapMethods).toEqual(["desktop-bootstrap"]);
36+
expect(descriptor.sessionCookieName).toBe("t3_session_3773");
3637
}).pipe(
3738
Effect.provide(
3839
makeServerAuthPolicyLayer({
3940
mode: "desktop",
41+
port: 3773,
4042
}),
4143
),
4244
),
@@ -66,6 +68,7 @@ it.layer(NodeServices.layer)("ServerAuthPolicyLive", (it) => {
6668

6769
expect(descriptor.policy).toBe("loopback-browser");
6870
expect(descriptor.bootstrapMethods).toEqual(["one-time-token"]);
71+
expect(descriptor.sessionCookieName).toBe("t3_session");
6972
}).pipe(
7073
Effect.provide(
7174
makeServerAuthPolicyLayer({

apps/server/src/auth/Layers/ServerAuthPolicy.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Effect, Layer } from "effect";
33

44
import { ServerConfig } from "../../config.ts";
55
import { ServerAuthPolicy, type ServerAuthPolicyShape } from "../Services/ServerAuthPolicy.ts";
6-
import { SESSION_COOKIE_NAME } from "../utils.ts";
6+
import { resolveSessionCookieName } from "../utils.ts";
77
import { isLoopbackHost, isWildcardHost } from "../../startupAccess.ts";
88

99
export const makeServerAuthPolicy = Effect.gen(function* () {
@@ -30,7 +30,10 @@ export const makeServerAuthPolicy = Effect.gen(function* () {
3030
policy,
3131
bootstrapMethods,
3232
sessionMethods: ["browser-session-cookie", "bearer-session-token"],
33-
sessionCookieName: SESSION_COOKIE_NAME,
33+
sessionCookieName: resolveSessionCookieName({
34+
mode: config.mode,
35+
port: config.port,
36+
}),
3437
};
3538

3639
return {

0 commit comments

Comments
 (0)