Skip to content

Commit 5a3b7be

Browse files
Merge upstream/main into main
2 parents b291f72 + 5467d11 commit 5a3b7be

28 files changed

+1666
-450
lines changed

.context/upstream-sync.md

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
## Status atual
44

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
5+
- Data: 2026-04-11
6+
- Branch de trabalho: `main`
7+
- Upstream integrado nesta wave: `5467d11980e2b41e4cf5c8d1c5fe972532da3a74` (`upstream/main`)
8+
- Estado: merge aplicado e validado localmente com `thread loop` e `file references` preservados
99

1010
## Features locais vivas
1111

@@ -43,3 +43,12 @@
4343
- Se a mudanca for UX de skill/slash command, tentar absorver do upstream primeiro
4444
- Se a mudanca for regra de negocio local, empurrar para `t3code-custom/*`
4545
- Se precisar tocar `ChatComposer` ou `ComposerPromptEditor`, fazer o minimo e deixar a adaptacao visivel
46+
47+
## 2026-04-11 — Sync 6 commits do upstream
48+
49+
- Merge de `upstream/main` de `e0e01b4a` ate `5467d119`
50+
- O unico conflito textual real apareceu em `apps/web/src/store.ts`; a resolucao adotou a derivacao memoizada nova do upstream em `apps/web/src/threadDerivation.ts`
51+
- Entraram melhorias do upstream em git e chat sem reabrir o fork do composer: diretorios git apagados deixam de quebrar a deteccao, links quebrados em varias linhas no terminal passam a abrir direito, o panel de pending user input para de roubar atalhos numericos de editores focados e mensagens do assistant agora podem ser copiadas com estado de streaming mais robusto
52+
- `MessagesTimeline.tsx` continuou respeitando o parser custom de `file references`; a mudanca do upstream entrou por cima sem derrubar o boundary local de sentinelas
53+
- `thread loop` continuou intacto no estado da thread: os campos `loop` no shell/store seguem vivos e os eventos `thread.loop-upserted` e `thread.loop-deleted` foram preservados
54+
- A principal reducao de atrito desta wave foi aceitar o `threadDerivation.ts` do upstream em vez de manter derivacao duplicada dentro de `store.ts`

apps/server/src/git/Layers/GitCore.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,24 @@ function writeTextFile(
4040
});
4141
}
4242

43+
function removePath(
44+
targetPath: string,
45+
): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> {
46+
return Effect.gen(function* () {
47+
const fileSystem = yield* FileSystem.FileSystem;
48+
yield* fileSystem.remove(targetPath, { recursive: true, force: true });
49+
});
50+
}
51+
52+
function makeDirectory(
53+
dirPath: string,
54+
): Effect.Effect<void, PlatformError.PlatformError, FileSystem.FileSystem> {
55+
return Effect.gen(function* () {
56+
const fileSystem = yield* FileSystem.FileSystem;
57+
yield* fileSystem.makeDirectory(dirPath, { recursive: true });
58+
});
59+
}
60+
4361
/** Run a raw git command for test setup (not under test). */
4462
function git(
4563
cwd: string,
@@ -299,6 +317,21 @@ it.layer(TestLayer)("git integration", (it) => {
299317
}),
300318
);
301319

320+
it.effect("returns isRepo: false for deleted directories", () =>
321+
Effect.gen(function* () {
322+
const tmp = yield* makeTmpDir();
323+
const deletedDir = path.join(tmp, "deleted-repo");
324+
yield* makeDirectory(deletedDir);
325+
yield* removePath(deletedDir);
326+
327+
const result = yield* (yield* GitCore).listBranches({ cwd: deletedDir });
328+
329+
expect(result.isRepo).toBe(false);
330+
expect(result.hasOriginRemote).toBe(false);
331+
expect(result.branches).toEqual([]);
332+
}),
333+
);
334+
302335
it.effect("returns the current branch with current: true", () =>
303336
Effect.gen(function* () {
304337
const tmp = yield* makeTmpDir();
@@ -1626,6 +1659,37 @@ it.layer(TestLayer)("git integration", (it) => {
16261659
}),
16271660
);
16281661

1662+
it.effect("returns a non-repo status for deleted directories", () =>
1663+
Effect.gen(function* () {
1664+
const tmp = yield* makeTmpDir();
1665+
const deletedDir = path.join(tmp, "deleted-repo");
1666+
yield* makeDirectory(deletedDir);
1667+
yield* removePath(deletedDir);
1668+
const core = yield* GitCore;
1669+
1670+
const status = yield* core.statusDetails(deletedDir);
1671+
const localStatus = yield* core.statusDetailsLocal(deletedDir);
1672+
1673+
expect(status).toEqual({
1674+
isRepo: false,
1675+
hasOriginRemote: false,
1676+
isDefaultBranch: false,
1677+
branch: null,
1678+
upstreamRef: null,
1679+
hasWorkingTreeChanges: false,
1680+
workingTree: {
1681+
files: [],
1682+
insertions: 0,
1683+
deletions: 0,
1684+
},
1685+
hasUpstream: false,
1686+
aheadCount: 0,
1687+
behindCount: 0,
1688+
});
1689+
expect(localStatus).toEqual(status);
1690+
}),
1691+
);
1692+
16291693
it.effect("computes ahead count against base branch when no upstream is configured", () =>
16301694
Effect.gen(function* () {
16311695
const tmp = yield* makeTmpDir();

apps/server/src/git/Layers/GitCore.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
type ExecuteGitProgress,
2828
type GitCommitOptions,
2929
type GitCoreShape,
30+
type GitStatusDetails,
3031
type ExecuteGitInput,
3132
type ExecuteGitResult,
3233
} from "../Services/GitCore.ts";
@@ -59,6 +60,18 @@ const STATUS_UPSTREAM_REFRESH_FAILURE_COOLDOWN = Duration.seconds(5);
5960
const STATUS_UPSTREAM_REFRESH_CACHE_CAPACITY = 2_048;
6061
const DEFAULT_BASE_BRANCH_CANDIDATES = ["main", "master"] as const;
6162
const GIT_LIST_BRANCHES_DEFAULT_LIMIT = 100;
63+
const NON_REPOSITORY_STATUS_DETAILS = Object.freeze<GitStatusDetails>({
64+
isRepo: false,
65+
hasOriginRemote: false,
66+
isDefaultBranch: false,
67+
branch: null,
68+
upstreamRef: null,
69+
hasWorkingTreeChanges: false,
70+
workingTree: { files: [], insertions: 0, deletions: 0 },
71+
hasUpstream: false,
72+
aheadCount: 0,
73+
behindCount: 0,
74+
});
6275

6376
type TraceTailState = {
6477
processedChars: number;
@@ -359,6 +372,16 @@ function quoteGitCommand(args: ReadonlyArray<string>): string {
359372
return `git ${args.join(" ")}`;
360373
}
361374

375+
function isMissingGitCwdError(error: GitCommandError): boolean {
376+
const normalized = `${error.detail}\n${error.message}`.toLowerCase();
377+
return (
378+
normalized.includes("no such file or directory") ||
379+
normalized.includes("notfound: filesystem.access") ||
380+
normalized.includes("enoent") ||
381+
normalized.includes("not a directory")
382+
);
383+
}
384+
362385
function toGitCommandError(
363386
input: Pick<ExecuteGitInput, "operation" | "cwd" | "args">,
364387
detail: string,
@@ -1190,7 +1213,11 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
11901213
{
11911214
allowNonZeroExit: true,
11921215
},
1193-
);
1216+
).pipe(Effect.catchIf(isMissingGitCwdError, () => Effect.succeed(null)));
1217+
1218+
if (statusResult === null) {
1219+
return NON_REPOSITORY_STATUS_DETAILS;
1220+
}
11941221

11951222
if (statusResult.code !== 0) {
11961223
const stderr = statusResult.stderr.trim();
@@ -1322,7 +1349,10 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
13221349
);
13231350

13241351
const statusDetails: GitCoreShape["statusDetails"] = Effect.fn("statusDetails")(function* (cwd) {
1325-
yield* refreshStatusUpstreamIfStale(cwd).pipe(Effect.ignoreCause({ log: true }));
1352+
yield* refreshStatusUpstreamIfStale(cwd).pipe(
1353+
Effect.catchIf(isMissingGitCwdError, () => Effect.void),
1354+
Effect.ignoreCause({ log: true }),
1355+
);
13261356
return yield* readStatusDetailsLocal(cwd);
13271357
});
13281358

@@ -1719,6 +1749,16 @@ export const makeGitCore = Effect.fn("makeGitCore")(function* (options?: {
17191749
timeoutMs: 10_000,
17201750
allowNonZeroExit: true,
17211751
},
1752+
).pipe(
1753+
Effect.catchIf(isMissingGitCwdError, () =>
1754+
Effect.succeed({
1755+
code: 128,
1756+
stdout: "",
1757+
stderr: "fatal: not a git repository",
1758+
stdoutTruncated: false,
1759+
stderrTruncated: false,
1760+
}),
1761+
),
17221762
);
17231763

17241764
if (localBranchResult.code !== 0) {

apps/server/src/git/Layers/GitHubCli.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,105 @@ layer("GitHubCliLive", (it) => {
7676
}),
7777
);
7878

79+
it.effect("trims pull request fields decoded from gh json", () =>
80+
Effect.gen(function* () {
81+
mockedRunProcess.mockResolvedValueOnce({
82+
stdout: JSON.stringify({
83+
number: 42,
84+
title: " Add PR thread creation \n",
85+
url: " https://github.com/pingdotgg/codething-mvp/pull/42 ",
86+
baseRefName: " main ",
87+
headRefName: "\tfeature/pr-threads\t",
88+
state: "OPEN",
89+
mergedAt: null,
90+
isCrossRepository: true,
91+
headRepository: {
92+
nameWithOwner: " octocat/codething-mvp ",
93+
},
94+
headRepositoryOwner: {
95+
login: " octocat ",
96+
},
97+
}),
98+
stderr: "",
99+
code: 0,
100+
signal: null,
101+
timedOut: false,
102+
});
103+
104+
const result = yield* Effect.gen(function* () {
105+
const gh = yield* GitHubCli;
106+
return yield* gh.getPullRequest({
107+
cwd: "/repo",
108+
reference: "#42",
109+
});
110+
});
111+
112+
assert.deepStrictEqual(result, {
113+
number: 42,
114+
title: "Add PR thread creation",
115+
url: "https://github.com/pingdotgg/codething-mvp/pull/42",
116+
baseRefName: "main",
117+
headRefName: "feature/pr-threads",
118+
state: "open",
119+
isCrossRepository: true,
120+
headRepositoryNameWithOwner: "octocat/codething-mvp",
121+
headRepositoryOwnerLogin: "octocat",
122+
});
123+
}),
124+
);
125+
126+
it.effect("skips invalid entries when parsing pr lists", () =>
127+
Effect.gen(function* () {
128+
mockedRunProcess.mockResolvedValueOnce({
129+
stdout: JSON.stringify([
130+
{
131+
number: 0,
132+
title: "invalid",
133+
url: "https://github.com/pingdotgg/codething-mvp/pull/0",
134+
baseRefName: "main",
135+
headRefName: "feature/invalid",
136+
},
137+
{
138+
number: 43,
139+
title: " Valid PR ",
140+
url: " https://github.com/pingdotgg/codething-mvp/pull/43 ",
141+
baseRefName: " main ",
142+
headRefName: " feature/pr-list ",
143+
headRepository: {
144+
nameWithOwner: " ",
145+
},
146+
headRepositoryOwner: {
147+
login: " ",
148+
},
149+
},
150+
]),
151+
stderr: "",
152+
code: 0,
153+
signal: null,
154+
timedOut: false,
155+
});
156+
157+
const result = yield* Effect.gen(function* () {
158+
const gh = yield* GitHubCli;
159+
return yield* gh.listOpenPullRequests({
160+
cwd: "/repo",
161+
headSelector: "feature/pr-list",
162+
});
163+
});
164+
165+
assert.deepStrictEqual(result, [
166+
{
167+
number: 43,
168+
title: "Valid PR",
169+
url: "https://github.com/pingdotgg/codething-mvp/pull/43",
170+
baseRefName: "main",
171+
headRefName: "feature/pr-list",
172+
state: "open",
173+
},
174+
]);
175+
}),
176+
);
177+
79178
it.effect("reads repository clone URLs", () =>
80179
Effect.gen(function* () {
81180
mockedRunProcess.mockResolvedValueOnce({

0 commit comments

Comments
 (0)