Skip to content

Commit cac0551

Browse files
authored
Merge branch 'main' into feat/codex-prolite-fix
2 parents 07e8590 + 569fea8 commit cac0551

71 files changed

Lines changed: 4140 additions & 1342 deletions

File tree

Some content is hidden

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

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ Observability guide: [docs/observability.md](./docs/observability.md)
4949

5050
## If you REALLY want to contribute still.... read this first
5151

52+
Before local development, prepare the environment and install dependencies:
53+
54+
```bash
55+
# Optional: only needed if you use mise for dev tool management.
56+
mise install
57+
bun install .
58+
```
59+
5260
Read [CONTRIBUTING.md](./CONTRIBUTING.md) before opening an issue or PR.
5361

5462
Need support? Join the [Discord](https://discord.gg/jn4EGJjrvv).

apps/server/src/checkpointing/Layers/CheckpointDiffQuery.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,15 @@ describe("CheckpointDiffQueryLive", () => {
8282
Layer.succeed(ProjectionSnapshotQuery, {
8383
getSnapshot: () =>
8484
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
85+
getShellSnapshot: () =>
86+
Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
8587
getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
8688
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
89+
getProjectShellById: () => Effect.succeed(Option.none()),
8790
getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
8891
getThreadCheckpointContext: () => Effect.succeed(Option.some(threadCheckpointContext)),
92+
getThreadShellById: () => Effect.succeed(Option.none()),
93+
getThreadDetailById: () => Effect.succeed(Option.none()),
8994
}),
9095
),
9196
);
@@ -136,10 +141,15 @@ describe("CheckpointDiffQueryLive", () => {
136141
Layer.succeed(ProjectionSnapshotQuery, {
137142
getSnapshot: () =>
138143
Effect.die("CheckpointDiffQuery should not request the full orchestration snapshot"),
144+
getShellSnapshot: () =>
145+
Effect.die("CheckpointDiffQuery should not request the orchestration shell snapshot"),
139146
getCounts: () => Effect.succeed({ projectCount: 0, threadCount: 0 }),
140147
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
148+
getProjectShellById: () => Effect.succeed(Option.none()),
141149
getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
142150
getThreadCheckpointContext: () => Effect.succeed(Option.none()),
151+
getThreadShellById: () => Effect.succeed(Option.none()),
152+
getThreadDetailById: () => Effect.succeed(Option.none()),
143153
}),
144154
),
145155
);

apps/server/src/open.test.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
4242
args: ["/tmp/workspace"],
4343
});
4444

45+
const kiroLaunch = yield* resolveEditorLaunch(
46+
{ cwd: "/tmp/workspace", editor: "kiro" },
47+
"darwin",
48+
{ PATH: "" },
49+
);
50+
assert.deepEqual(kiroLaunch, {
51+
command: "kiro",
52+
args: ["ide", "/tmp/workspace"],
53+
});
54+
4555
const vscodeLaunch = yield* resolveEditorLaunch(
4656
{ cwd: "/tmp/workspace", editor: "vscode" },
4757
"darwin",
@@ -122,6 +132,16 @@ it.layer(NodeServices.layer)("resolveEditorLaunch", (it) => {
122132
args: ["--goto", "/tmp/workspace/src/open.ts:71:5"],
123133
});
124134

135+
const kiroLineAndColumn = yield* resolveEditorLaunch(
136+
{ cwd: "/tmp/workspace/src/open.ts:71:5", editor: "kiro" },
137+
"darwin",
138+
{ PATH: "" },
139+
);
140+
assert.deepEqual(kiroLineAndColumn, {
141+
command: "kiro",
142+
args: ["ide", "--goto", "/tmp/workspace/src/open.ts:71:5"],
143+
});
144+
125145
const vscodeLineAndColumn = yield* resolveEditorLaunch(
126146
{ cwd: "/tmp/workspace/src/open.ts:71:5", editor: "vscode" },
127147
"darwin",
@@ -354,14 +374,15 @@ it.layer(NodeServices.layer)("resolveAvailableEditors", (it) => {
354374
const dir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-editors-" });
355375

356376
yield* fs.writeFileString(path.join(dir, "trae.CMD"), "@echo off\r\n");
377+
yield* fs.writeFileString(path.join(dir, "kiro.CMD"), "@echo off\r\n");
357378
yield* fs.writeFileString(path.join(dir, "code-insiders.CMD"), "@echo off\r\n");
358379
yield* fs.writeFileString(path.join(dir, "codium.CMD"), "@echo off\r\n");
359380
yield* fs.writeFileString(path.join(dir, "explorer.CMD"), "MZ");
360381
const editors = resolveAvailableEditors("win32", {
361382
PATH: dir,
362383
PATHEXT: ".COM;.EXE;.BAT;.CMD",
363384
});
364-
assert.deepEqual(editors, ["trae", "vscode-insiders", "vscodium", "file-manager"]);
385+
assert.deepEqual(editors, ["trae", "kiro", "vscode-insiders", "vscodium", "file-manager"]);
365386
}),
366387
);
367388

apps/server/src/open.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ function resolveCommandEditorArgs(
7575
}
7676
}
7777

78+
function resolveEditorArgs(
79+
editor: (typeof EDITORS)[number],
80+
target: string,
81+
): ReadonlyArray<string> {
82+
const baseArgs = "baseArgs" in editor ? editor.baseArgs : [];
83+
return [...baseArgs, ...resolveCommandEditorArgs(editor, target)];
84+
}
85+
7886
function resolveAvailableCommand(
7987
commands: ReadonlyArray<string>,
8088
options: CommandAvailabilityOptions = {},
@@ -273,7 +281,7 @@ export const resolveEditorLaunch = Effect.fn("resolveEditorLaunch")(function* (
273281
resolveAvailableCommand(editorDef.commands, { platform, env }) ?? editorDef.commands[0];
274282
return {
275283
command,
276-
args: resolveCommandEditorArgs(editorDef, input.cwd),
284+
args: resolveEditorArgs(editorDef, input.cwd),
277285
};
278286
}
279287

apps/server/src/orchestration/Layers/OrchestrationEngine.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,10 +144,20 @@ describe("OrchestrationEngine", () => {
144144
Layer.provide(
145145
Layer.succeed(ProjectionSnapshotQuery, {
146146
getSnapshot: () => Effect.succeed(projectionSnapshot),
147+
getShellSnapshot: () =>
148+
Effect.succeed({
149+
snapshotSequence: projectionSnapshot.snapshotSequence,
150+
projects: [],
151+
threads: [],
152+
updatedAt: projectionSnapshot.updatedAt,
153+
}),
147154
getCounts: () => Effect.succeed({ projectCount: 1, threadCount: 1 }),
148155
getActiveProjectByWorkspaceRoot: () => Effect.succeed(Option.none()),
156+
getProjectShellById: () => Effect.succeed(Option.none()),
149157
getFirstActiveThreadIdByProjectId: () => Effect.succeed(Option.none()),
150158
getThreadCheckpointContext: () => Effect.succeed(Option.none()),
159+
getThreadShellById: () => Effect.succeed(Option.none()),
160+
getThreadDetailById: () => Effect.succeed(Option.none()),
151161
}),
152162
),
153163
Layer.provide(

apps/server/src/orchestration/Layers/ProjectionPipeline.ts

Lines changed: 125 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
ApprovalRequestId,
33
type ChatAttachment,
44
type OrchestrationEvent,
5+
ThreadId,
56
} from "@t3tools/contracts";
67
import { Effect, FileSystem, Layer, Option, Path, Stream } from "effect";
78
import * as SqlClient from "effect/unstable/sql/SqlClient";
@@ -89,6 +90,77 @@ function extractActivityRequestId(payload: unknown): ApprovalRequestId | null {
8990
return typeof requestId === "string" ? ApprovalRequestId.make(requestId) : null;
9091
}
9192

93+
function derivePendingUserInputCountFromActivities(
94+
activities: ReadonlyArray<ProjectionThreadActivity>,
95+
): number {
96+
const openRequestIds = new Set<string>();
97+
const ordered = [...activities].toSorted(
98+
(left, right) =>
99+
left.createdAt.localeCompare(right.createdAt) ||
100+
left.activityId.localeCompare(right.activityId),
101+
);
102+
103+
for (const activity of ordered) {
104+
const requestId = extractActivityRequestId(activity.payload);
105+
if (requestId === null) {
106+
continue;
107+
}
108+
const payload =
109+
typeof activity.payload === "object" && activity.payload !== null
110+
? (activity.payload as Record<string, unknown>)
111+
: null;
112+
const detail = typeof payload?.detail === "string" ? payload.detail.toLowerCase() : null;
113+
114+
if (activity.kind === "user-input.requested") {
115+
openRequestIds.add(requestId);
116+
continue;
117+
}
118+
119+
if (activity.kind === "user-input.resolved") {
120+
openRequestIds.delete(requestId);
121+
continue;
122+
}
123+
124+
if (
125+
activity.kind === "provider.user-input.respond.failed" &&
126+
detail !== null &&
127+
(detail.includes("stale pending user-input request") ||
128+
detail.includes("unknown pending user-input request"))
129+
) {
130+
openRequestIds.delete(requestId);
131+
}
132+
}
133+
134+
return openRequestIds.size;
135+
}
136+
137+
function deriveHasActionableProposedPlan(input: {
138+
readonly latestTurnId: string | null;
139+
readonly proposedPlans: ReadonlyArray<ProjectionThreadProposedPlan>;
140+
}): boolean {
141+
const sorted = [...input.proposedPlans].toSorted(
142+
(left, right) =>
143+
left.updatedAt.localeCompare(right.updatedAt) || left.planId.localeCompare(right.planId),
144+
);
145+
146+
let latestForTurn: ProjectionThreadProposedPlan | null = null;
147+
if (input.latestTurnId !== null) {
148+
for (let index = sorted.length - 1; index >= 0; index -= 1) {
149+
const plan = sorted[index];
150+
if (plan?.turnId === input.latestTurnId) {
151+
latestForTurn = plan;
152+
break;
153+
}
154+
}
155+
}
156+
if (latestForTurn !== null) {
157+
return latestForTurn.implementedAt === null;
158+
}
159+
160+
const latestPlan = sorted.at(-1) ?? null;
161+
return latestPlan !== null && latestPlan.implementedAt === null;
162+
}
163+
92164
function retainProjectionMessagesAfterRevert(
93165
messages: ReadonlyArray<ProjectionThreadMessage>,
94166
turns: ReadonlyArray<ProjectionTurn>,
@@ -432,6 +504,48 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
432504
}
433505
});
434506

507+
const refreshThreadShellSummary = Effect.fn("refreshThreadShellSummary")(function* (
508+
threadId: ThreadId,
509+
) {
510+
const existingRow = yield* projectionThreadRepository.getById({
511+
threadId,
512+
});
513+
if (Option.isNone(existingRow)) {
514+
return;
515+
}
516+
517+
const [messages, proposedPlans, activities, pendingApprovals] = yield* Effect.all([
518+
projectionThreadMessageRepository.listByThreadId({ threadId }),
519+
projectionThreadProposedPlanRepository.listByThreadId({ threadId }),
520+
projectionThreadActivityRepository.listByThreadId({ threadId }),
521+
projectionPendingApprovalRepository.listByThreadId({ threadId }),
522+
]);
523+
524+
const latestUserMessageAt =
525+
messages
526+
.filter((message) => message.role === "user")
527+
.map((message) => message.createdAt)
528+
.toSorted()
529+
.at(-1) ?? null;
530+
531+
const pendingApprovalCount = pendingApprovals.filter(
532+
(approval) => approval.status === "pending",
533+
).length;
534+
const pendingUserInputCount = derivePendingUserInputCountFromActivities(activities);
535+
const hasActionableProposedPlan = deriveHasActionableProposedPlan({
536+
latestTurnId: existingRow.value.latestTurnId,
537+
proposedPlans,
538+
});
539+
540+
yield* projectionThreadRepository.upsert({
541+
...existingRow.value,
542+
latestUserMessageAt,
543+
pendingApprovalCount,
544+
pendingUserInputCount,
545+
hasActionableProposedPlan: hasActionableProposedPlan ? 1 : 0,
546+
});
547+
});
548+
435549
const applyThreadsProjection: ProjectorDefinition["apply"] = Effect.fn(
436550
"applyThreadsProjection",
437551
)(function* (event, attachmentSideEffects) {
@@ -450,6 +564,10 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
450564
createdAt: event.payload.createdAt,
451565
updatedAt: event.payload.updatedAt,
452566
archivedAt: null,
567+
latestUserMessageAt: null,
568+
pendingApprovalCount: 0,
569+
pendingUserInputCount: 0,
570+
hasActionableProposedPlan: 0,
453571
deletedAt: null,
454572
});
455573
return;
@@ -554,7 +672,9 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
554672

555673
case "thread.message-sent":
556674
case "thread.proposed-plan-upserted":
557-
case "thread.activity-appended": {
675+
case "thread.activity-appended":
676+
case "thread.approval-response-requested":
677+
case "thread.user-input-response-requested": {
558678
const existingRow = yield* projectionThreadRepository.getById({
559679
threadId: event.payload.threadId,
560680
});
@@ -565,6 +685,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
565685
...existingRow.value,
566686
updatedAt: event.occurredAt,
567687
});
688+
yield* refreshThreadShellSummary(event.payload.threadId);
568689
return;
569690
}
570691

@@ -580,6 +701,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
580701
latestTurnId: event.payload.session.activeTurnId,
581702
updatedAt: event.occurredAt,
582703
});
704+
yield* refreshThreadShellSummary(event.payload.threadId);
583705
return;
584706
}
585707

@@ -595,6 +717,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
595717
latestTurnId: event.payload.turnId,
596718
updatedAt: event.occurredAt,
597719
});
720+
yield* refreshThreadShellSummary(event.payload.threadId);
598721
return;
599722
}
600723

@@ -610,6 +733,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
610733
latestTurnId: null,
611734
updatedAt: event.occurredAt,
612735
});
736+
yield* refreshThreadShellSummary(event.payload.threadId);
613737
return;
614738
}
615739

0 commit comments

Comments
 (0)