Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
32 changes: 31 additions & 1 deletion apps/mobile/src/state/use-thread-pr.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
import {
createLinkedPullRequestDetailAtomFamily,
pullRequestDetailToVcsStatus,
} from "@t3tools/client-runtime/state/pull-requests";

import { connectionAtomRuntime } from "../connection/runtime";
import { useEnvironmentQuery } from "./query";
import { presentThreadPr, type ThreadPrPresentation } from "./thread-pr-presentation";
import { vcsEnvironment } from "./vcs";

const linkedPullRequestDetailAtom = createLinkedPullRequestDetailAtomFamily(connectionAtomRuntime);

export {
presentThreadPr,
type ThreadPr,
Expand All @@ -22,13 +29,36 @@ export function useThreadPr(
): ThreadPrPresentation | null {
const cwd = thread.worktreePath ?? projectCwd;
const gitStatus = useEnvironmentQuery(
thread.branch !== null && cwd !== null
thread.linkedPullRequest == null && thread.branch !== null && cwd !== null
? vcsEnvironment.status({
environmentId: thread.environmentId,
input: { cwd },
})
: null,
);
const linkedPullRequest = useEnvironmentQuery(
thread.linkedPullRequest == null
? null
: linkedPullRequestDetailAtom({
environmentId: thread.environmentId,
input: {
projectId: thread.linkedPullRequest.projectId,
repository: thread.linkedPullRequest.repository,
number: thread.linkedPullRequest.number,
},
}),
);

if (thread.linkedPullRequest != null) {
const detail = linkedPullRequest.data;
return detail === null
? null
: presentThreadPr(pullRequestDetailToVcsStatus(detail), {
kind: detail.provider,
name: detail.provider,
baseUrl: new URL(detail.url).origin,
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Outdated
});
Comment thread
t3dotgg marked this conversation as resolved.
}

const status = gitStatus.data;
if (status === null || thread.branch === null || status.refName !== thread.branch) {
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ it.layer(NodeServices.layer)("ServerEnvironmentLive", (it) => {
expect(second.capabilities.attachmentUploads).toBe(true);
expect(second.capabilities.pullRequests).toBe(true);
expect(second.capabilities.threadTitleRegeneration).toBe(true);
expect(second.capabilities.threadPullRequestLinking).toBe(true);
expect(second.capabilities.agentActivityPublishing).toBe(false);
}),
);
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ export const make = Effect.gen(function* () {
threadPinning: true,
threadPinReorder: true,
threadTitleRegeneration: true,
threadPullRequestLinking: true,
...(serverSelfUpdate === null ? {} : { serverSelfUpdate }),
...(serverSelfUpdate === "boot-service" ? { serverSelfUpdateProgress: true } : {}),
},
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
interactionMode: event.payload.interactionMode,
branch: event.payload.branch,
worktreePath: event.payload.worktreePath,
linkedPullRequest: null,
latestTurnId: null,
createdAt: event.payload.createdAt,
updatedAt: event.payload.updatedAt,
Expand Down Expand Up @@ -822,6 +823,9 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
...(event.payload.worktreePath !== undefined
? { worktreePath: event.payload.worktreePath }
: {}),
...(event.payload.linkedPullRequest !== undefined
? { linkedPullRequest: event.payload.linkedPullRequest }
: {}),
updatedAt: event.payload.updatedAt,
});
return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
interaction_mode,
branch,
worktree_path,
linked_pull_request_json,
latest_turn_id,
latest_user_message_at,
pending_approval_count,
Expand All @@ -102,6 +103,7 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
'default',
NULL,
NULL,
'{"projectId":"project-1","repository":"pingdotgg/t3code","number":42,"url":"https://github.com/pingdotgg/t3code/pull/42"}',
'turn-1',
'2026-02-24T00:00:04.000Z',
1,
Expand Down Expand Up @@ -304,6 +306,12 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
runtimeMode: "full-access",
branch: null,
worktreePath: null,
linkedPullRequest: {
projectId: asProjectId("project-1"),
repository: "pingdotgg/t3code",
number: 42,
url: "https://github.com/pingdotgg/t3code/pull/42",
},
latestTurn: {
turnId: asTurnId("turn-1"),
state: "completed",
Expand Down Expand Up @@ -423,6 +431,12 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
runtimeMode: "full-access",
branch: null,
worktreePath: null,
linkedPullRequest: {
projectId: asProjectId("project-1"),
repository: "pingdotgg/t3code",
number: 42,
url: "https://github.com/pingdotgg/t3code/pull/42",
},
latestTurn: {
turnId: asTurnId("turn-1"),
state: "completed",
Expand Down
24 changes: 24 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
type OrchestrationThreadShell,
ModelSelection,
ProjectId,
ThreadLinkedPullRequest,
ThreadId,
} from "@t3tools/contracts";
import * as Arr from "effect/Array";
Expand Down Expand Up @@ -89,6 +90,7 @@ const ProjectionThreadProposedPlanDbRowSchema = ProjectionThreadProposedPlan;
const ProjectionThreadDbRowSchema = ProjectionThread.mapFields(
Struct.assign({
modelSelection: Schema.fromJsonString(ModelSelection),
linkedPullRequest: Schema.NullOr(Schema.fromJsonString(ThreadLinkedPullRequest)),
}),
);
const ProjectionThreadActivityDbRowSchema = ProjectionThreadActivity.mapFields(
Expand Down Expand Up @@ -422,6 +424,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interaction_mode AS "interactionMode",
branch,
worktree_path AS "worktreePath",
linked_pull_request_json AS "linkedPullRequest",
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -458,6 +461,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interaction_mode AS "interactionMode",
branch,
worktree_path AS "worktreePath",
linked_pull_request_json AS "linkedPullRequest",
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -496,6 +500,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interaction_mode AS "interactionMode",
branch,
worktree_path AS "worktreePath",
linked_pull_request_json AS "linkedPullRequest",
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -938,6 +943,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interaction_mode AS "interactionMode",
branch,
worktree_path AS "worktreePath",
linked_pull_request_json AS "linkedPullRequest",
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -1694,6 +1700,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: row.interactionMode,
branch: row.branch,
worktreePath: row.worktreePath,
...(row.linkedPullRequest === null
? {}
: { linkedPullRequest: row.linkedPullRequest }),
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -1901,6 +1910,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: row.interactionMode,
branch: row.branch,
worktreePath: row.worktreePath,
...(row.linkedPullRequest === null
? {}
: { linkedPullRequest: row.linkedPullRequest }),
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -2037,6 +2049,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: row.interactionMode,
branch: row.branch,
worktreePath: row.worktreePath,
...(row.linkedPullRequest === null
? {}
: { linkedPullRequest: row.linkedPullRequest }),
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -2182,6 +2197,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: row.interactionMode,
branch: row.branch,
worktreePath: row.worktreePath,
...(row.linkedPullRequest === null
? {}
: { linkedPullRequest: row.linkedPullRequest }),
latestTurn: latestTurnByThread.get(row.threadId) ?? null,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
Expand Down Expand Up @@ -2461,6 +2479,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: threadRow.value.interactionMode,
branch: threadRow.value.branch,
worktreePath: threadRow.value.worktreePath,
...(threadRow.value.linkedPullRequest === null
? {}
: { linkedPullRequest: threadRow.value.linkedPullRequest }),
latestTurn: Option.isSome(latestTurnRow) ? mapLatestTurn(latestTurnRow.value) : null,
createdAt: threadRow.value.createdAt,
updatedAt: threadRow.value.updatedAt,
Expand Down Expand Up @@ -2602,6 +2623,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
interactionMode: threadRow.value.interactionMode,
branch: threadRow.value.branch,
worktreePath: threadRow.value.worktreePath,
...(threadRow.value.linkedPullRequest === null
? {}
: { linkedPullRequest: threadRow.value.linkedPullRequest }),
latestTurn: Option.isSome(latestTurnRow) ? mapLatestTurn(latestTurnRow.value) : null,
createdAt: threadRow.value.createdAt,
updatedAt: threadRow.value.updatedAt,
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/orchestration/decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,9 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
: {}),
...(branch !== undefined ? { branch } : {}),
...(command.worktreePath !== undefined ? { worktreePath: command.worktreePath } : {}),
...(command.linkedPullRequest !== undefined
? { linkedPullRequest: command.linkedPullRequest }
: {}),
updatedAt: occurredAt,
},
};
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/orchestration/projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,9 @@ export function projectEvent(
: {}),
...(payload.branch !== undefined ? { branch: payload.branch } : {}),
...(payload.worktreePath !== undefined ? { worktreePath: payload.worktreePath } : {}),
...(payload.linkedPullRequest !== undefined
? { linkedPullRequest: payload.linkedPullRequest }
: {}),
updatedAt: payload.updatedAt,
}),
})),
Expand Down
51 changes: 51 additions & 0 deletions apps/server/src/persistence/Layers/ProjectionRepositories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,55 @@ projectionRepositoriesLayer("Projection repositories", (it) => {
assert.strictEqual(updated?.pinnedAt, null);
}),
);

it.effect("round-trips a linked pull request through the thread row", () =>
Effect.gen(function* () {
const threads = yield* ProjectionThreadRepository;
const linkedPullRequest = {
projectId: ProjectId.make("project-linked-pr"),
repository: "pingdotgg/t3code",
number: 42,
url: "https://github.com/pingdotgg/t3code/pull/42",
};

yield* threads.upsert({
threadId: ThreadId.make("thread-linked-pr"),
projectId: ProjectId.make("project-linked-pr"),
title: "Linked pull request",
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5.4",
},
runtimeMode: "full-access",
interactionMode: "default",
branch: null,
worktreePath: null,
linkedPullRequest,
latestTurnId: null,
createdAt: "2026-03-24T00:00:00.000Z",
updatedAt: "2026-03-24T00:00:00.000Z",
archivedAt: null,
settledOverride: null,
settledAt: null,
snoozedUntil: null,
snoozedAt: null,
pinnedAt: null,
latestUserMessageAt: null,
pendingApprovalCount: 0,
pendingUserInputCount: 0,
hasActionableProposedPlan: 0,
deletedAt: null,
});

const persisted = yield* threads.getById({ threadId: ThreadId.make("thread-linked-pr") });
assert.deepStrictEqual(Option.getOrNull(persisted)?.linkedPullRequest, linkedPullRequest);

const row = Option.getOrNull(persisted);
if (row === null) return yield* Effect.die("Expected linked thread row to exist.");
yield* threads.upsert({ ...row, linkedPullRequest: null });

const cleared = yield* threads.getById({ threadId: ThreadId.make("thread-linked-pr") });
assert.strictEqual(Option.getOrNull(cleared)?.linkedPullRequest, null);
}),
);
});
8 changes: 7 additions & 1 deletion apps/server/src/persistence/Layers/ProjectionThreads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ import {
ProjectionThreadRepository,
type ProjectionThreadRepositoryShape,
} from "../Services/ProjectionThreads.ts";
import { ModelSelection } from "@t3tools/contracts";
import { ModelSelection, ThreadLinkedPullRequest } from "@t3tools/contracts";

const ProjectionThreadDbRow = ProjectionThread.mapFields(
Struct.assign({
modelSelection: Schema.fromJsonString(ModelSelection),
linkedPullRequest: Schema.NullOr(Schema.fromJsonString(ThreadLinkedPullRequest)),
}),
);
type ProjectionThreadDbRow = typeof ProjectionThreadDbRow.Type;
Expand All @@ -39,6 +40,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
interaction_mode,
branch,
worktree_path,
linked_pull_request_json,
latest_turn_id,
created_at,
updated_at,
Expand Down Expand Up @@ -66,6 +68,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
${row.interactionMode},
${row.branch},
${row.worktreePath},
${row.linkedPullRequest === undefined || row.linkedPullRequest === null ? null : JSON.stringify(row.linkedPullRequest)},
${row.latestTurnId},
${row.createdAt},
${row.updatedAt},
Expand Down Expand Up @@ -93,6 +96,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
interaction_mode = excluded.interaction_mode,
branch = excluded.branch,
worktree_path = excluded.worktree_path,
linked_pull_request_json = excluded.linked_pull_request_json,
latest_turn_id = excluded.latest_turn_id,
created_at = excluded.created_at,
updated_at = excluded.updated_at,
Expand Down Expand Up @@ -127,6 +131,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
interaction_mode AS "interactionMode",
branch,
worktree_path AS "worktreePath",
linked_pull_request_json AS "linkedPullRequest",
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down Expand Up @@ -163,6 +168,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
interaction_mode AS "interactionMode",
branch,
worktree_path AS "worktreePath",
linked_pull_request_json AS "linkedPullRequest",
latest_turn_id AS "latestTurnId",
created_at AS "createdAt",
updated_at AS "updatedAt",
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/persistence/Migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import Migration0038 from "./Migrations/038_ProjectionThreadsPinOrderKey.ts";
import Migration0039 from "./Migrations/039_ProjectionProjectsDefaultThreadEnvMode.ts";
import Migration0040 from "./Migrations/040_ProjectionProjectFaviconPath.ts";
import Migration0041 from "./Migrations/041_AuthSessionClientConnection.ts";
import Migration0042 from "./Migrations/042_ProjectionThreadLinkedPullRequest.ts";

/**
* Migration loader with all migrations defined inline.
Expand Down Expand Up @@ -107,6 +108,7 @@ export const migrationEntries = [
[39, "ProjectionProjectsDefaultThreadEnvMode", Migration0039],
[40, "ProjectionProjectFaviconPath", Migration0040],
[41, "AuthSessionClientConnection", Migration0041],
[42, "ProjectionThreadLinkedPullRequest", Migration0042],
] as const;

export const migrationManifest = migrationEntries.map(([id, name]) => [id, name] as const);
Expand Down
Loading
Loading