webmux 0.35.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/dist/server.js +1496 -542
- package/bin/webmux.js +1689 -1418
- package/frontend/dist/assets/DiffDialog-npkRTiLF.js +112 -0
- package/frontend/dist/assets/index-CwsEmpEK.js +36 -0
- package/frontend/dist/assets/index-XCRBR8rv.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/frontend/dist/assets/DiffDialog-BpDYIOfR.js +0 -112
- package/frontend/dist/assets/index-CU9BHgDe.js +0 -89
- package/frontend/dist/assets/index-DaGuNXKA.css +0 -1
package/backend/dist/server.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var __defProp = Object.defineProperty;
|
|
3
3
|
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
|
4
|
+
var __returnValue = (v) => v;
|
|
5
|
+
function __exportSetter(name, newValue) {
|
|
6
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
7
|
+
}
|
|
4
8
|
var __export = (target, all) => {
|
|
5
9
|
for (var name in all)
|
|
6
10
|
__defProp(target, name, {
|
|
7
11
|
get: all[name],
|
|
8
12
|
enumerable: true,
|
|
9
13
|
configurable: true,
|
|
10
|
-
set: (
|
|
14
|
+
set: __exportSetter.bind(all, name)
|
|
11
15
|
});
|
|
12
16
|
};
|
|
13
17
|
var __require = import.meta.require;
|
|
@@ -6961,7 +6965,7 @@ import { networkInterfaces } from "os";
|
|
|
6961
6965
|
// package.json
|
|
6962
6966
|
var package_default = {
|
|
6963
6967
|
name: "webmux",
|
|
6964
|
-
version: "0.
|
|
6968
|
+
version: "0.37.0",
|
|
6965
6969
|
description: "Web dashboard for workmux \u2014 browser UI with embedded terminals, PR monitoring, and CI integration",
|
|
6966
6970
|
type: "module",
|
|
6967
6971
|
repository: {
|
|
@@ -10990,7 +10994,7 @@ var coerce = {
|
|
|
10990
10994
|
date: (arg) => ZodDate.create({ ...arg, coerce: true })
|
|
10991
10995
|
};
|
|
10992
10996
|
var NEVER = INVALID;
|
|
10993
|
-
// node_modules/.bun/@ts-rest+core@3.52.1+
|
|
10997
|
+
// node_modules/.bun/@ts-rest+core@3.52.1+0e383980587f1470/node_modules/@ts-rest/core/index.esm.mjs
|
|
10994
10998
|
var isZodObjectStrict = (obj) => {
|
|
10995
10999
|
return typeof (obj === null || obj === undefined ? undefined : obj.passthrough) === "function";
|
|
10996
11000
|
};
|
|
@@ -11388,17 +11392,24 @@ var AgentsUiWorktreeSummarySchema = exports_external.object({
|
|
|
11388
11392
|
conversation: WorktreeConversationRefSchema.nullable()
|
|
11389
11393
|
});
|
|
11390
11394
|
var AgentsUiConversationMessageRoleSchema = exports_external.enum(["user", "assistant"]);
|
|
11391
|
-
var AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress"]);
|
|
11392
|
-
var AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "toolUse", "toolResult"]);
|
|
11395
|
+
var AgentsUiConversationMessageStatusSchema = exports_external.enum(["completed", "inProgress", "failed"]);
|
|
11396
|
+
var AgentsUiConversationMessageKindSchema = exports_external.enum(["text", "thinking", "toolUse", "toolResult"]);
|
|
11393
11397
|
var AgentsUiConversationMessageSchema = exports_external.object({
|
|
11394
11398
|
id: exports_external.string(),
|
|
11395
11399
|
turnId: exports_external.string(),
|
|
11400
|
+
order: exports_external.number().int().nonnegative(),
|
|
11396
11401
|
role: AgentsUiConversationMessageRoleSchema,
|
|
11397
11402
|
text: exports_external.string(),
|
|
11398
11403
|
status: AgentsUiConversationMessageStatusSchema,
|
|
11399
11404
|
createdAt: exports_external.string().nullable(),
|
|
11400
|
-
kind: AgentsUiConversationMessageKindSchema
|
|
11401
|
-
|
|
11405
|
+
kind: AgentsUiConversationMessageKindSchema,
|
|
11406
|
+
phase: exports_external.string().optional(),
|
|
11407
|
+
toolName: exports_external.string().optional(),
|
|
11408
|
+
toolCallId: exports_external.string().optional(),
|
|
11409
|
+
command: exports_external.string().optional(),
|
|
11410
|
+
cwd: exports_external.string().optional(),
|
|
11411
|
+
exitCode: exports_external.number().nullable().optional(),
|
|
11412
|
+
durationMs: exports_external.number().nullable().optional()
|
|
11402
11413
|
});
|
|
11403
11414
|
var AgentsUiConversationStateSchema = exports_external.object({
|
|
11404
11415
|
provider: WorktreeConversationProviderSchema,
|
|
@@ -11422,24 +11433,36 @@ var AgentsUiInterruptResponseSchema = exports_external.object({
|
|
|
11422
11433
|
turnId: exports_external.string(),
|
|
11423
11434
|
interrupted: exports_external.literal(true)
|
|
11424
11435
|
});
|
|
11425
|
-
var AgentsUiConversationSnapshotEventSchema = exports_external.object({
|
|
11426
|
-
type: exports_external.literal("snapshot"),
|
|
11427
|
-
data: AgentsUiWorktreeConversationResponseSchema
|
|
11428
|
-
});
|
|
11429
11436
|
var AgentsUiConversationMessageDeltaEventSchema = exports_external.object({
|
|
11430
11437
|
type: exports_external.literal("messageDelta"),
|
|
11438
|
+
revision: exports_external.number().int().nonnegative(),
|
|
11431
11439
|
conversationId: exports_external.string(),
|
|
11432
11440
|
turnId: exports_external.string(),
|
|
11433
11441
|
itemId: exports_external.string(),
|
|
11442
|
+
order: exports_external.number().int().nonnegative(),
|
|
11434
11443
|
delta: exports_external.string()
|
|
11435
11444
|
});
|
|
11445
|
+
var AgentsUiConversationMessageUpsertEventSchema = exports_external.object({
|
|
11446
|
+
type: exports_external.literal("messageUpsert"),
|
|
11447
|
+
revision: exports_external.number().int().nonnegative(),
|
|
11448
|
+
conversationId: exports_external.string(),
|
|
11449
|
+
message: AgentsUiConversationMessageSchema
|
|
11450
|
+
});
|
|
11451
|
+
var AgentsUiConversationStatusEventSchema = exports_external.object({
|
|
11452
|
+
type: exports_external.literal("conversationStatus"),
|
|
11453
|
+
revision: exports_external.number().int().nonnegative(),
|
|
11454
|
+
conversationId: exports_external.string(),
|
|
11455
|
+
running: exports_external.boolean(),
|
|
11456
|
+
activeTurnId: exports_external.string().nullable()
|
|
11457
|
+
});
|
|
11436
11458
|
var AgentsUiConversationErrorEventSchema = exports_external.object({
|
|
11437
11459
|
type: exports_external.literal("error"),
|
|
11438
11460
|
message: exports_external.string()
|
|
11439
11461
|
});
|
|
11440
11462
|
var AgentsUiConversationEventSchema = exports_external.discriminatedUnion("type", [
|
|
11441
|
-
AgentsUiConversationSnapshotEventSchema,
|
|
11442
11463
|
AgentsUiConversationMessageDeltaEventSchema,
|
|
11464
|
+
AgentsUiConversationMessageUpsertEventSchema,
|
|
11465
|
+
AgentsUiConversationStatusEventSchema,
|
|
11443
11466
|
AgentsUiConversationErrorEventSchema
|
|
11444
11467
|
]);
|
|
11445
11468
|
var WorktreeListResponseSchema = exports_external.object({
|
|
@@ -12845,6 +12868,14 @@ function isStringArray(raw) {
|
|
|
12845
12868
|
// backend/src/adapters/codex-app-server.ts
|
|
12846
12869
|
var CodexAppServerApprovalPolicySchema = exports_external.enum(["untrusted", "on-failure", "on-request", "never"]);
|
|
12847
12870
|
var UnknownValueSchema = exports_external.custom(() => true);
|
|
12871
|
+
var JsonValueSchema = exports_external.union([
|
|
12872
|
+
exports_external.string(),
|
|
12873
|
+
exports_external.number(),
|
|
12874
|
+
exports_external.boolean(),
|
|
12875
|
+
exports_external.null(),
|
|
12876
|
+
exports_external.array(UnknownValueSchema),
|
|
12877
|
+
exports_external.record(UnknownValueSchema)
|
|
12878
|
+
]);
|
|
12848
12879
|
var CodexAppServerContentItemSchema = exports_external.object({
|
|
12849
12880
|
type: exports_external.string(),
|
|
12850
12881
|
text: exports_external.string().optional()
|
|
@@ -12859,9 +12890,103 @@ var CodexAppServerAgentMessageItemSchema = exports_external.object({
|
|
|
12859
12890
|
id: exports_external.string(),
|
|
12860
12891
|
text: exports_external.string().optional(),
|
|
12861
12892
|
message: exports_external.string().optional(),
|
|
12862
|
-
phase: exports_external.string().optional(),
|
|
12893
|
+
phase: exports_external.string().nullable().optional(),
|
|
12863
12894
|
memoryCitation: UnknownValueSchema.optional()
|
|
12864
12895
|
});
|
|
12896
|
+
var CodexAppServerCommandActionSchema = exports_external.object({
|
|
12897
|
+
type: exports_external.string(),
|
|
12898
|
+
command: exports_external.string().optional(),
|
|
12899
|
+
path: exports_external.string().nullable().optional()
|
|
12900
|
+
});
|
|
12901
|
+
var CodexAppServerCommandExecutionItemSchema = exports_external.object({
|
|
12902
|
+
type: exports_external.literal("commandExecution"),
|
|
12903
|
+
id: exports_external.string(),
|
|
12904
|
+
command: exports_external.string(),
|
|
12905
|
+
cwd: exports_external.string().nullable(),
|
|
12906
|
+
status: exports_external.enum(["inProgress", "completed", "failed", "declined"]),
|
|
12907
|
+
commandActions: exports_external.array(CodexAppServerCommandActionSchema).default([]),
|
|
12908
|
+
aggregatedOutput: exports_external.string().nullable(),
|
|
12909
|
+
exitCode: exports_external.number().nullable(),
|
|
12910
|
+
durationMs: exports_external.number().nullable()
|
|
12911
|
+
});
|
|
12912
|
+
var CodexAppServerPatchChangeKindSchema = exports_external.union([
|
|
12913
|
+
exports_external.object({ type: exports_external.literal("add") }),
|
|
12914
|
+
exports_external.object({ type: exports_external.literal("delete") }),
|
|
12915
|
+
exports_external.object({ type: exports_external.literal("update"), move_path: exports_external.string().nullable() })
|
|
12916
|
+
]);
|
|
12917
|
+
var CodexAppServerFileUpdateChangeSchema = exports_external.object({
|
|
12918
|
+
path: exports_external.string(),
|
|
12919
|
+
kind: CodexAppServerPatchChangeKindSchema,
|
|
12920
|
+
diff: exports_external.string()
|
|
12921
|
+
});
|
|
12922
|
+
var CodexAppServerFileChangeItemSchema = exports_external.object({
|
|
12923
|
+
type: exports_external.literal("fileChange"),
|
|
12924
|
+
id: exports_external.string(),
|
|
12925
|
+
changes: exports_external.array(CodexAppServerFileUpdateChangeSchema),
|
|
12926
|
+
status: exports_external.enum(["inProgress", "completed", "failed", "declined"])
|
|
12927
|
+
});
|
|
12928
|
+
var CodexAppServerMcpToolCallResultSchema = exports_external.object({
|
|
12929
|
+
content: exports_external.array(JsonValueSchema),
|
|
12930
|
+
structuredContent: JsonValueSchema,
|
|
12931
|
+
_meta: JsonValueSchema
|
|
12932
|
+
});
|
|
12933
|
+
var CodexAppServerMcpToolCallErrorSchema = exports_external.object({
|
|
12934
|
+
message: exports_external.string()
|
|
12935
|
+
});
|
|
12936
|
+
var CodexAppServerMcpToolCallItemSchema = exports_external.object({
|
|
12937
|
+
type: exports_external.literal("mcpToolCall"),
|
|
12938
|
+
id: exports_external.string(),
|
|
12939
|
+
server: exports_external.string(),
|
|
12940
|
+
tool: exports_external.string(),
|
|
12941
|
+
status: exports_external.enum(["inProgress", "completed", "failed"]),
|
|
12942
|
+
arguments: JsonValueSchema,
|
|
12943
|
+
mcpAppResourceUri: exports_external.string().optional(),
|
|
12944
|
+
pluginId: exports_external.string().nullable(),
|
|
12945
|
+
result: CodexAppServerMcpToolCallResultSchema.nullable(),
|
|
12946
|
+
error: CodexAppServerMcpToolCallErrorSchema.nullable(),
|
|
12947
|
+
durationMs: exports_external.number().nullable()
|
|
12948
|
+
});
|
|
12949
|
+
var CodexAppServerDynamicToolCallContentItemSchema = exports_external.union([
|
|
12950
|
+
exports_external.object({ type: exports_external.literal("inputText"), text: exports_external.string() }),
|
|
12951
|
+
exports_external.object({ type: exports_external.literal("inputImage"), imageUrl: exports_external.string() })
|
|
12952
|
+
]);
|
|
12953
|
+
var CodexAppServerDynamicToolCallItemSchema = exports_external.object({
|
|
12954
|
+
type: exports_external.literal("dynamicToolCall"),
|
|
12955
|
+
id: exports_external.string(),
|
|
12956
|
+
namespace: exports_external.string().nullable(),
|
|
12957
|
+
tool: exports_external.string(),
|
|
12958
|
+
arguments: JsonValueSchema,
|
|
12959
|
+
status: exports_external.enum(["inProgress", "completed", "failed"]),
|
|
12960
|
+
contentItems: exports_external.array(CodexAppServerDynamicToolCallContentItemSchema).nullable(),
|
|
12961
|
+
success: exports_external.boolean().nullable(),
|
|
12962
|
+
durationMs: exports_external.number().nullable()
|
|
12963
|
+
});
|
|
12964
|
+
var CodexAppServerWebSearchActionSchema = exports_external.union([
|
|
12965
|
+
exports_external.object({ type: exports_external.literal("search"), query: exports_external.string().nullable(), queries: exports_external.array(exports_external.string()).nullable() }),
|
|
12966
|
+
exports_external.object({ type: exports_external.literal("openPage"), url: exports_external.string().nullable() }),
|
|
12967
|
+
exports_external.object({ type: exports_external.literal("findInPage"), url: exports_external.string().nullable(), pattern: exports_external.string().nullable() }),
|
|
12968
|
+
exports_external.object({ type: exports_external.literal("other") })
|
|
12969
|
+
]);
|
|
12970
|
+
var CodexAppServerWebSearchItemSchema = exports_external.object({
|
|
12971
|
+
type: exports_external.literal("webSearch"),
|
|
12972
|
+
id: exports_external.string(),
|
|
12973
|
+
query: exports_external.string(),
|
|
12974
|
+
action: CodexAppServerWebSearchActionSchema.nullable()
|
|
12975
|
+
});
|
|
12976
|
+
var CodexAppServerIgnoredItemSchema = exports_external.object({
|
|
12977
|
+
type: exports_external.enum([
|
|
12978
|
+
"hookPrompt",
|
|
12979
|
+
"plan",
|
|
12980
|
+
"reasoning",
|
|
12981
|
+
"collabAgentToolCall",
|
|
12982
|
+
"imageView",
|
|
12983
|
+
"imageGeneration",
|
|
12984
|
+
"enteredReviewMode",
|
|
12985
|
+
"exitedReviewMode",
|
|
12986
|
+
"contextCompaction"
|
|
12987
|
+
]),
|
|
12988
|
+
id: exports_external.string()
|
|
12989
|
+
});
|
|
12865
12990
|
var CodexAppServerGenericItemSchema = exports_external.object({
|
|
12866
12991
|
type: exports_external.string(),
|
|
12867
12992
|
id: exports_external.string()
|
|
@@ -12869,6 +12994,12 @@ var CodexAppServerGenericItemSchema = exports_external.object({
|
|
|
12869
12994
|
var CodexAppServerThreadItemSchema = exports_external.union([
|
|
12870
12995
|
CodexAppServerUserMessageItemSchema,
|
|
12871
12996
|
CodexAppServerAgentMessageItemSchema,
|
|
12997
|
+
CodexAppServerCommandExecutionItemSchema,
|
|
12998
|
+
CodexAppServerFileChangeItemSchema,
|
|
12999
|
+
CodexAppServerMcpToolCallItemSchema,
|
|
13000
|
+
CodexAppServerDynamicToolCallItemSchema,
|
|
13001
|
+
CodexAppServerWebSearchItemSchema,
|
|
13002
|
+
CodexAppServerIgnoredItemSchema,
|
|
12872
13003
|
CodexAppServerGenericItemSchema
|
|
12873
13004
|
]);
|
|
12874
13005
|
var CodexAppServerTurnSchema = exports_external.object({
|
|
@@ -12958,7 +13089,31 @@ var CodexAppServerInitializeResponseSchema = exports_external.object({
|
|
|
12958
13089
|
platformFamily: exports_external.string(),
|
|
12959
13090
|
platformOs: exports_external.string()
|
|
12960
13091
|
});
|
|
12961
|
-
|
|
13092
|
+
function readCodexAppServerStdoutLines(input) {
|
|
13093
|
+
let buffer = input.buffer + (input.chunk ? input.decoder.decode(input.chunk, { stream: true }) : input.decoder.decode());
|
|
13094
|
+
const lines = [];
|
|
13095
|
+
while (true) {
|
|
13096
|
+
const newlineIndex = buffer.indexOf(`
|
|
13097
|
+
`);
|
|
13098
|
+
if (newlineIndex === -1)
|
|
13099
|
+
break;
|
|
13100
|
+
const line = buffer.slice(0, newlineIndex).trim();
|
|
13101
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
13102
|
+
if (line.length > 0)
|
|
13103
|
+
lines.push(line);
|
|
13104
|
+
}
|
|
13105
|
+
if (!input.chunk) {
|
|
13106
|
+
const finalLine = buffer.trim();
|
|
13107
|
+
buffer = "";
|
|
13108
|
+
if (finalLine.length > 0)
|
|
13109
|
+
lines.push(finalLine);
|
|
13110
|
+
}
|
|
13111
|
+
return { buffer, lines };
|
|
13112
|
+
}
|
|
13113
|
+
function parseCodexAppServerThreadItem(raw) {
|
|
13114
|
+
const parsed = CodexAppServerThreadItemSchema.safeParse(raw);
|
|
13115
|
+
return parsed.success ? parsed.data : null;
|
|
13116
|
+
}
|
|
12962
13117
|
class CodexAppServerRequestError extends Error {
|
|
12963
13118
|
code;
|
|
12964
13119
|
data;
|
|
@@ -12972,7 +13127,6 @@ class CodexAppServerRequestError extends Error {
|
|
|
12972
13127
|
class CodexAppServerClient {
|
|
12973
13128
|
opts;
|
|
12974
13129
|
encoder = new TextEncoder;
|
|
12975
|
-
decoder = new TextDecoder;
|
|
12976
13130
|
listeners = new Set;
|
|
12977
13131
|
pending = new Map;
|
|
12978
13132
|
nextId = 1;
|
|
@@ -13059,25 +13213,30 @@ class CodexAppServerClient {
|
|
|
13059
13213
|
startStdoutLoop(proc) {
|
|
13060
13214
|
(async () => {
|
|
13061
13215
|
const reader = proc.stdout.getReader();
|
|
13216
|
+
const decoder = new TextDecoder;
|
|
13062
13217
|
let buffer = "";
|
|
13063
13218
|
try {
|
|
13064
13219
|
while (true) {
|
|
13065
13220
|
const { done, value } = await reader.read();
|
|
13066
13221
|
if (done)
|
|
13067
13222
|
break;
|
|
13068
|
-
|
|
13069
|
-
|
|
13070
|
-
|
|
13071
|
-
|
|
13072
|
-
|
|
13073
|
-
|
|
13074
|
-
|
|
13075
|
-
buffer = buffer.slice(newlineIndex + 1);
|
|
13076
|
-
if (line.length === 0)
|
|
13077
|
-
continue;
|
|
13223
|
+
const decoded2 = readCodexAppServerStdoutLines({
|
|
13224
|
+
decoder,
|
|
13225
|
+
buffer,
|
|
13226
|
+
chunk: value
|
|
13227
|
+
});
|
|
13228
|
+
buffer = decoded2.buffer;
|
|
13229
|
+
for (const line of decoded2.lines) {
|
|
13078
13230
|
this.handleStdoutLine(line);
|
|
13079
13231
|
}
|
|
13080
13232
|
}
|
|
13233
|
+
const decoded = readCodexAppServerStdoutLines({
|
|
13234
|
+
decoder,
|
|
13235
|
+
buffer
|
|
13236
|
+
});
|
|
13237
|
+
for (const line of decoded.lines) {
|
|
13238
|
+
this.handleStdoutLine(line);
|
|
13239
|
+
}
|
|
13081
13240
|
} catch (error) {
|
|
13082
13241
|
if (this.proc === proc) {
|
|
13083
13242
|
log.error("[agents] codex app-server stdout reader failed", error);
|
|
@@ -13088,12 +13247,13 @@ class CodexAppServerClient {
|
|
|
13088
13247
|
startStderrLoop(proc) {
|
|
13089
13248
|
(async () => {
|
|
13090
13249
|
const reader = proc.stderr.getReader();
|
|
13250
|
+
const decoder = new TextDecoder;
|
|
13091
13251
|
try {
|
|
13092
13252
|
while (true) {
|
|
13093
13253
|
const { done, value } = await reader.read();
|
|
13094
13254
|
if (done)
|
|
13095
13255
|
break;
|
|
13096
|
-
const chunk =
|
|
13256
|
+
const chunk = decoder.decode(value, { stream: true }).trim();
|
|
13097
13257
|
if (chunk.length > 0) {
|
|
13098
13258
|
log.debug(`[agents] codex app-server stderr: ${chunk}`);
|
|
13099
13259
|
}
|
|
@@ -14723,14 +14883,26 @@ async function createLinearIssue(input) {
|
|
|
14723
14883
|
}
|
|
14724
14884
|
|
|
14725
14885
|
// backend/src/services/conversation-export-service.ts
|
|
14886
|
+
var WebmuxConversationAttachmentMessageSchema = AgentsUiConversationMessageSchema.extend({
|
|
14887
|
+
order: exports_external.number().int().nonnegative().optional(),
|
|
14888
|
+
kind: AgentsUiConversationMessageKindSchema.optional()
|
|
14889
|
+
});
|
|
14726
14890
|
var WebmuxConversationAttachmentPayloadSchema = exports_external.object({
|
|
14727
14891
|
webmux: exports_external.literal(1),
|
|
14728
14892
|
branch: exports_external.string(),
|
|
14729
14893
|
baseBranch: exports_external.string().nullable(),
|
|
14730
14894
|
agent: AgentIdSchema.nullable(),
|
|
14731
14895
|
createdAt: exports_external.string(),
|
|
14732
|
-
conversation: exports_external.array(
|
|
14896
|
+
conversation: exports_external.array(WebmuxConversationAttachmentMessageSchema).transform((messages) => messages.map((message, order) => ({
|
|
14897
|
+
...message,
|
|
14898
|
+
order: message.order ?? order,
|
|
14899
|
+
kind: message.kind ?? "text"
|
|
14900
|
+
})))
|
|
14733
14901
|
});
|
|
14902
|
+
function parseWebmuxConversationAttachmentPayload(raw) {
|
|
14903
|
+
const parsed = WebmuxConversationAttachmentPayloadSchema.safeParse(raw);
|
|
14904
|
+
return parsed.success ? parsed.data : null;
|
|
14905
|
+
}
|
|
14734
14906
|
var defaultSeedFromLinearDeps = {
|
|
14735
14907
|
fetchIssueWithAttachments,
|
|
14736
14908
|
downloadWebmuxAttachment: downloadWebmuxAttachmentDefault
|
|
@@ -14907,11 +15079,11 @@ async function downloadWebmuxAttachmentDefault(url) {
|
|
|
14907
15079
|
return { ok: false, error: `Asset download failed ${res.status}` };
|
|
14908
15080
|
}
|
|
14909
15081
|
const text = await res.text();
|
|
14910
|
-
const parsed =
|
|
14911
|
-
if (!parsed
|
|
15082
|
+
const parsed = parseWebmuxConversationAttachmentPayload(JSON.parse(text));
|
|
15083
|
+
if (!parsed) {
|
|
14912
15084
|
return { ok: false, error: "Asset is not a webmux conversation payload" };
|
|
14913
15085
|
}
|
|
14914
|
-
return { ok: true, data: parsed
|
|
15086
|
+
return { ok: true, data: parsed };
|
|
14915
15087
|
} catch (err) {
|
|
14916
15088
|
const msg = err instanceof Error ? err.message : String(err);
|
|
14917
15089
|
return { ok: false, error: msg };
|
|
@@ -15500,9 +15672,9 @@ class BunTmuxGateway {
|
|
|
15500
15672
|
|
|
15501
15673
|
// backend/src/domain/policies.ts
|
|
15502
15674
|
var INVALID_BRANCH_CHARS_RE = /[~^:?*\[\]\\]+/g;
|
|
15503
|
-
var UNSAFE_ENV_KEY_RE = /^[
|
|
15504
|
-
var VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]
|
|
15505
|
-
var VALID_INSTANCE_PREFIX_RE = /^[a-z0-9][a-z0-9\-]
|
|
15675
|
+
var UNSAFE_ENV_KEY_RE = /^[a-z_][a-z0-9_]*$/i;
|
|
15676
|
+
var VALID_WORKTREE_NAME_RE = /^[a-z0-9][a-z0-9\-_./]*$/i;
|
|
15677
|
+
var VALID_INSTANCE_PREFIX_RE = /^[a-z0-9][a-z0-9\-]*$/i;
|
|
15506
15678
|
function sanitizeBranchName(raw) {
|
|
15507
15679
|
return raw.toLowerCase().replace(/\s+/g, "-").replace(INVALID_BRANCH_CHARS_RE, "").replace(/@\{/g, "").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/-{2,}/g, "-").replace(/^[.\-/]+|[.\-/]+$/g, "").replace(/\.lock$/i, "");
|
|
15508
15680
|
}
|
|
@@ -17769,184 +17941,6 @@ function startAutoPullMonitor(deps, intervalMs) {
|
|
|
17769
17941
|
return startSerializedInterval(run, intervalMs);
|
|
17770
17942
|
}
|
|
17771
17943
|
|
|
17772
|
-
// backend/src/services/agents-ui-stream-service.ts
|
|
17773
|
-
function readNotificationParams(raw) {
|
|
17774
|
-
return isRecord3(raw) ? raw : null;
|
|
17775
|
-
}
|
|
17776
|
-
function readThreadId(raw) {
|
|
17777
|
-
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
17778
|
-
}
|
|
17779
|
-
function readNotificationItemType(raw) {
|
|
17780
|
-
if (!isRecord3(raw))
|
|
17781
|
-
return null;
|
|
17782
|
-
return typeof raw.type === "string" ? raw.type : null;
|
|
17783
|
-
}
|
|
17784
|
-
function readAgentsNotificationThreadId(notification) {
|
|
17785
|
-
const params = readNotificationParams(notification.params);
|
|
17786
|
-
if (!params)
|
|
17787
|
-
return null;
|
|
17788
|
-
return readThreadId(params.threadId);
|
|
17789
|
-
}
|
|
17790
|
-
function buildAgentsUiMessageDeltaEvent(notification) {
|
|
17791
|
-
if (notification.method !== "item/agentMessage/delta")
|
|
17792
|
-
return null;
|
|
17793
|
-
const params = readNotificationParams(notification.params);
|
|
17794
|
-
if (!params)
|
|
17795
|
-
return null;
|
|
17796
|
-
const threadId = readThreadId(params.threadId);
|
|
17797
|
-
const turnId = readThreadId(params.turnId);
|
|
17798
|
-
const itemId = readThreadId(params.itemId);
|
|
17799
|
-
const delta = typeof params.delta === "string" ? params.delta : null;
|
|
17800
|
-
if (!threadId || !turnId || !itemId || delta === null)
|
|
17801
|
-
return null;
|
|
17802
|
-
return {
|
|
17803
|
-
type: "messageDelta",
|
|
17804
|
-
conversationId: threadId,
|
|
17805
|
-
turnId,
|
|
17806
|
-
itemId,
|
|
17807
|
-
delta
|
|
17808
|
-
};
|
|
17809
|
-
}
|
|
17810
|
-
function shouldRefreshAgentsConversationSnapshot(notification) {
|
|
17811
|
-
switch (notification.method) {
|
|
17812
|
-
case "turn/started":
|
|
17813
|
-
case "turn/completed":
|
|
17814
|
-
case "thread/status/changed":
|
|
17815
|
-
return readAgentsNotificationThreadId(notification) !== null;
|
|
17816
|
-
case "item/completed": {
|
|
17817
|
-
const params = readNotificationParams(notification.params);
|
|
17818
|
-
if (!params)
|
|
17819
|
-
return false;
|
|
17820
|
-
const itemType = readNotificationItemType(params.item);
|
|
17821
|
-
return itemType === "userMessage" || itemType === "agentMessage";
|
|
17822
|
-
}
|
|
17823
|
-
default:
|
|
17824
|
-
return false;
|
|
17825
|
-
}
|
|
17826
|
-
}
|
|
17827
|
-
|
|
17828
|
-
// backend/src/services/agents-ui-action-service.ts
|
|
17829
|
-
function classifyAgentsTerminalWorktreeError(error) {
|
|
17830
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
17831
|
-
if (message.startsWith("No open tmux window found for worktree: ")) {
|
|
17832
|
-
return { status: 409, error: message };
|
|
17833
|
-
}
|
|
17834
|
-
if (message.startsWith("Worktree not found: ")) {
|
|
17835
|
-
return { status: 404, error: message };
|
|
17836
|
-
}
|
|
17837
|
-
return null;
|
|
17838
|
-
}
|
|
17839
|
-
|
|
17840
|
-
// backend/src/services/snapshot-service.ts
|
|
17841
|
-
function formatElapsedSince(startedAt, now) {
|
|
17842
|
-
if (!startedAt)
|
|
17843
|
-
return "";
|
|
17844
|
-
const startedMs = Date.parse(startedAt);
|
|
17845
|
-
if (Number.isNaN(startedMs))
|
|
17846
|
-
return "";
|
|
17847
|
-
const diffMs = Math.max(0, now().getTime() - startedMs);
|
|
17848
|
-
const diffMinutes = Math.floor(diffMs / 60000);
|
|
17849
|
-
if (diffMinutes < 1)
|
|
17850
|
-
return "0m";
|
|
17851
|
-
if (diffMinutes < 60)
|
|
17852
|
-
return `${diffMinutes}m`;
|
|
17853
|
-
const diffHours = Math.floor(diffMinutes / 60);
|
|
17854
|
-
if (diffHours < 24)
|
|
17855
|
-
return `${diffHours}h`;
|
|
17856
|
-
const diffDays = Math.floor(diffHours / 24);
|
|
17857
|
-
return `${diffDays}d`;
|
|
17858
|
-
}
|
|
17859
|
-
function clonePrEntry(pr) {
|
|
17860
|
-
return {
|
|
17861
|
-
...pr,
|
|
17862
|
-
ciChecks: pr.ciChecks.map((check) => ({ ...check })),
|
|
17863
|
-
comments: pr.comments.map((comment) => ({ ...comment }))
|
|
17864
|
-
};
|
|
17865
|
-
}
|
|
17866
|
-
function mapCreationSnapshot(creating) {
|
|
17867
|
-
return creating ? {
|
|
17868
|
-
phase: creating.phase
|
|
17869
|
-
} : null;
|
|
17870
|
-
}
|
|
17871
|
-
function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue, findAgentLabel) {
|
|
17872
|
-
return {
|
|
17873
|
-
branch: state.branch,
|
|
17874
|
-
label: state.label,
|
|
17875
|
-
...state.baseBranch ? { baseBranch: state.baseBranch } : {},
|
|
17876
|
-
path: state.path,
|
|
17877
|
-
dir: state.path,
|
|
17878
|
-
archived: isArchived(state.path),
|
|
17879
|
-
profile: state.profile,
|
|
17880
|
-
agentName: state.agentName,
|
|
17881
|
-
agentLabel: findAgentLabel ? findAgentLabel(state.agentName) : state.agentName,
|
|
17882
|
-
agentTerminalStale: state.agentTerminalStale,
|
|
17883
|
-
mux: state.session.exists,
|
|
17884
|
-
dirty: state.git.dirty,
|
|
17885
|
-
unpushed: state.git.aheadCount > 0,
|
|
17886
|
-
paneCount: state.session.paneCount,
|
|
17887
|
-
status: creating ? "creating" : state.agent.lifecycle,
|
|
17888
|
-
elapsed: formatElapsedSince(state.agent.lastStartedAt, now),
|
|
17889
|
-
services: state.services.map((service) => ({ ...service })),
|
|
17890
|
-
prs: state.prs.map((pr) => clonePrEntry(pr)),
|
|
17891
|
-
linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
|
|
17892
|
-
creation: mapCreationSnapshot(creating),
|
|
17893
|
-
source: state.source,
|
|
17894
|
-
oneshot: state.oneshot
|
|
17895
|
-
};
|
|
17896
|
-
}
|
|
17897
|
-
function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
|
|
17898
|
-
return {
|
|
17899
|
-
branch: creating.branch,
|
|
17900
|
-
label: null,
|
|
17901
|
-
...creating.baseBranch ? { baseBranch: creating.baseBranch } : {},
|
|
17902
|
-
path: creating.path,
|
|
17903
|
-
dir: creating.path,
|
|
17904
|
-
archived: isArchived(creating.path),
|
|
17905
|
-
profile: creating.profile,
|
|
17906
|
-
agentName: creating.agentName,
|
|
17907
|
-
agentLabel: findAgentLabel ? findAgentLabel(creating.agentName) : creating.agentName,
|
|
17908
|
-
agentTerminalStale: false,
|
|
17909
|
-
mux: false,
|
|
17910
|
-
dirty: false,
|
|
17911
|
-
unpushed: false,
|
|
17912
|
-
paneCount: 0,
|
|
17913
|
-
status: "creating",
|
|
17914
|
-
elapsed: "",
|
|
17915
|
-
services: [],
|
|
17916
|
-
prs: [],
|
|
17917
|
-
linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
|
|
17918
|
-
creation: mapCreationSnapshot(creating),
|
|
17919
|
-
source: creating.source,
|
|
17920
|
-
oneshot: null
|
|
17921
|
-
};
|
|
17922
|
-
}
|
|
17923
|
-
function buildWorktreeSnapshots(input) {
|
|
17924
|
-
const now = input.now ?? (() => new Date);
|
|
17925
|
-
const isArchived = input.isArchived ?? (() => false);
|
|
17926
|
-
const creatingWorktrees = input.creatingWorktrees ?? [];
|
|
17927
|
-
const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
|
|
17928
|
-
const runtimeWorktrees = input.runtime.listWorktrees();
|
|
17929
|
-
const runtimeBranches = new Set(runtimeWorktrees.map((worktree) => worktree.branch));
|
|
17930
|
-
const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue, input.findAgentLabel));
|
|
17931
|
-
for (const creating of creatingWorktrees) {
|
|
17932
|
-
if (!runtimeBranches.has(creating.branch)) {
|
|
17933
|
-
worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue, input.findAgentLabel));
|
|
17934
|
-
}
|
|
17935
|
-
}
|
|
17936
|
-
worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
|
|
17937
|
-
return worktrees;
|
|
17938
|
-
}
|
|
17939
|
-
function buildProjectSnapshot(input) {
|
|
17940
|
-
return {
|
|
17941
|
-
project: {
|
|
17942
|
-
name: input.projectName,
|
|
17943
|
-
mainBranch: input.mainBranch
|
|
17944
|
-
},
|
|
17945
|
-
worktrees: buildWorktreeSnapshots(input),
|
|
17946
|
-
notifications: input.notifications.map((notification) => ({ ...notification }))
|
|
17947
|
-
};
|
|
17948
|
-
}
|
|
17949
|
-
|
|
17950
17944
|
// backend/src/services/agents-ui-service.ts
|
|
17951
17945
|
function cloneConversationMeta(meta) {
|
|
17952
17946
|
return meta ? { ...meta } : null;
|
|
@@ -17977,43 +17971,1257 @@ function buildAgentsUiWorktreeSummary(worktree, conversation) {
|
|
|
17977
17971
|
};
|
|
17978
17972
|
}
|
|
17979
17973
|
|
|
17980
|
-
// backend/src/services/
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
function err(status, error) {
|
|
17985
|
-
return { ok: false, status, error };
|
|
17986
|
-
}
|
|
17987
|
-
|
|
17988
|
-
// backend/src/services/claude-conversation-service.ts
|
|
17989
|
-
function isClaudeWorktree(worktree) {
|
|
17990
|
-
return worktree.agentName === "claude";
|
|
17991
|
-
}
|
|
17992
|
-
function isClaudeConversationMeta(meta) {
|
|
17993
|
-
return meta?.provider === "claudeCode";
|
|
17994
|
-
}
|
|
17995
|
-
function buildPendingConversationId(worktree) {
|
|
17996
|
-
return `claude-pending:${worktree.path}`;
|
|
17974
|
+
// backend/src/services/codex-session-log-service.ts
|
|
17975
|
+
var TOOL_OUTPUT_TRUNCATE_LIMIT = 12000;
|
|
17976
|
+
function readString2(raw) {
|
|
17977
|
+
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
17997
17978
|
}
|
|
17998
|
-
function
|
|
17979
|
+
function parseLogLine(line) {
|
|
17980
|
+
let parsed;
|
|
17981
|
+
try {
|
|
17982
|
+
parsed = JSON.parse(line);
|
|
17983
|
+
} catch {
|
|
17984
|
+
return null;
|
|
17985
|
+
}
|
|
17986
|
+
if (!isRecord3(parsed))
|
|
17987
|
+
return null;
|
|
17999
17988
|
return {
|
|
18000
|
-
|
|
18001
|
-
|
|
18002
|
-
|
|
18003
|
-
cwd,
|
|
18004
|
-
lastSeenAt: now.toISOString()
|
|
17989
|
+
timestamp: readString2(parsed.timestamp),
|
|
17990
|
+
type: readString2(parsed.type),
|
|
17991
|
+
payload: isRecord3(parsed.payload) ? parsed.payload : null
|
|
18005
17992
|
};
|
|
18006
17993
|
}
|
|
18007
|
-
function
|
|
18008
|
-
|
|
17994
|
+
function truncate2(text, limit = TOOL_OUTPUT_TRUNCATE_LIMIT) {
|
|
17995
|
+
if (text.length <= limit)
|
|
17996
|
+
return text;
|
|
17997
|
+
return `${text.slice(0, limit)}... (truncated, ${text.length - limit} more chars)`;
|
|
18009
17998
|
}
|
|
18010
|
-
function
|
|
18011
|
-
|
|
17999
|
+
function compactJson2(value) {
|
|
18000
|
+
try {
|
|
18001
|
+
return JSON.stringify(value);
|
|
18002
|
+
} catch {
|
|
18003
|
+
return String(value);
|
|
18004
|
+
}
|
|
18005
|
+
}
|
|
18006
|
+
function readReasoningSummary(raw) {
|
|
18007
|
+
if (!Array.isArray(raw))
|
|
18008
|
+
return "";
|
|
18009
|
+
return raw.map((entry) => {
|
|
18010
|
+
if (typeof entry === "string")
|
|
18011
|
+
return entry;
|
|
18012
|
+
if (!isRecord3(entry))
|
|
18013
|
+
return "";
|
|
18014
|
+
if (typeof entry.text === "string")
|
|
18015
|
+
return entry.text;
|
|
18016
|
+
if (typeof entry.summary === "string")
|
|
18017
|
+
return entry.summary;
|
|
18018
|
+
return "";
|
|
18019
|
+
}).filter((text) => text.trim().length > 0).join(`
|
|
18020
|
+
`).trim();
|
|
18021
|
+
}
|
|
18022
|
+
function parseArgumentsRecord(raw) {
|
|
18023
|
+
if (!raw)
|
|
18024
|
+
return null;
|
|
18025
|
+
let parsed;
|
|
18026
|
+
try {
|
|
18027
|
+
parsed = JSON.parse(raw);
|
|
18028
|
+
} catch {
|
|
18029
|
+
return null;
|
|
18030
|
+
}
|
|
18031
|
+
return isRecord3(parsed) ? parsed : null;
|
|
18032
|
+
}
|
|
18033
|
+
function readToolCommand(toolName, argumentsText) {
|
|
18034
|
+
if (toolName === "apply_patch")
|
|
18035
|
+
return "apply_patch";
|
|
18036
|
+
const args = parseArgumentsRecord(argumentsText);
|
|
18037
|
+
if (!args)
|
|
18038
|
+
return null;
|
|
18039
|
+
if (toolName === "exec_command" && typeof args.cmd === "string")
|
|
18040
|
+
return args.cmd;
|
|
18041
|
+
return null;
|
|
18042
|
+
}
|
|
18043
|
+
function readToolCwd(argumentsText) {
|
|
18044
|
+
const args = parseArgumentsRecord(argumentsText);
|
|
18045
|
+
if (!args)
|
|
18046
|
+
return null;
|
|
18047
|
+
return typeof args.workdir === "string" ? args.workdir : null;
|
|
18048
|
+
}
|
|
18049
|
+
function buildToolUseText(toolName, argumentsText) {
|
|
18050
|
+
const command = readToolCommand(toolName, argumentsText);
|
|
18051
|
+
if (command)
|
|
18052
|
+
return command;
|
|
18053
|
+
return argumentsText?.trim() ?? "";
|
|
18054
|
+
}
|
|
18055
|
+
function readOutputExitCode(output) {
|
|
18056
|
+
const processMatch = output.match(/Process exited with code (-?\d+)/);
|
|
18057
|
+
if (processMatch?.[1])
|
|
18058
|
+
return Number(processMatch[1]);
|
|
18059
|
+
const exitMatch = output.match(/^Exit code: (-?\d+)/m);
|
|
18060
|
+
if (exitMatch?.[1])
|
|
18061
|
+
return Number(exitMatch[1]);
|
|
18062
|
+
return null;
|
|
18063
|
+
}
|
|
18064
|
+
function readOutputStatus(output) {
|
|
18065
|
+
const exitCode = readOutputExitCode(output);
|
|
18066
|
+
if (exitCode !== null)
|
|
18067
|
+
return exitCode === 0 ? "completed" : "failed";
|
|
18068
|
+
return output.startsWith("apply_patch verification failed") ? "failed" : "completed";
|
|
18069
|
+
}
|
|
18070
|
+
function pushMessage(messages, message) {
|
|
18071
|
+
messages.push({
|
|
18072
|
+
...message,
|
|
18073
|
+
order: messages.length
|
|
18074
|
+
});
|
|
18075
|
+
}
|
|
18076
|
+
function hasDuplicateTextMessage(input) {
|
|
18077
|
+
return input.messages.some((message) => message.turnId === input.turnId && message.role === input.role && message.kind === "text" && message.text === input.text && message.phase === input.phase);
|
|
18078
|
+
}
|
|
18079
|
+
function finalizeToolStatuses(messages) {
|
|
18080
|
+
const resultByCallId = new Map;
|
|
18081
|
+
for (const message of messages) {
|
|
18082
|
+
if (message.kind === "toolResult" && message.toolCallId) {
|
|
18083
|
+
resultByCallId.set(message.toolCallId, message);
|
|
18084
|
+
}
|
|
18085
|
+
}
|
|
18086
|
+
return messages.map((message) => {
|
|
18087
|
+
if (message.kind !== "toolUse" || !message.toolCallId)
|
|
18088
|
+
return message;
|
|
18089
|
+
const result = resultByCallId.get(message.toolCallId);
|
|
18090
|
+
if (!result)
|
|
18091
|
+
return message;
|
|
18092
|
+
return {
|
|
18093
|
+
...message,
|
|
18094
|
+
status: result.status,
|
|
18095
|
+
...result.exitCode !== undefined ? { exitCode: result.exitCode } : {},
|
|
18096
|
+
...result.durationMs !== undefined ? { durationMs: result.durationMs } : {}
|
|
18097
|
+
};
|
|
18098
|
+
});
|
|
18099
|
+
}
|
|
18100
|
+
function parseCodexSessionMessages(text) {
|
|
18101
|
+
const messages = [];
|
|
18102
|
+
const toolCallMetadata = new Map;
|
|
18103
|
+
let currentTurnId = null;
|
|
18104
|
+
let blockIndex = 0;
|
|
18105
|
+
for (const line of text.split(`
|
|
18106
|
+
`)) {
|
|
18107
|
+
const trimmed = line.trim();
|
|
18108
|
+
if (trimmed.length === 0)
|
|
18109
|
+
continue;
|
|
18110
|
+
const record = parseLogLine(trimmed);
|
|
18111
|
+
if (!record?.payload)
|
|
18112
|
+
continue;
|
|
18113
|
+
if (record.type === "event_msg") {
|
|
18114
|
+
const eventType = readString2(record.payload.type);
|
|
18115
|
+
if (eventType === "task_started") {
|
|
18116
|
+
currentTurnId = readString2(record.payload.turn_id);
|
|
18117
|
+
blockIndex = 0;
|
|
18118
|
+
continue;
|
|
18119
|
+
}
|
|
18120
|
+
if (eventType === "task_complete" || eventType === "turn_aborted") {
|
|
18121
|
+
currentTurnId = null;
|
|
18122
|
+
continue;
|
|
18123
|
+
}
|
|
18124
|
+
if (eventType === "user_message" && currentTurnId) {
|
|
18125
|
+
const text2 = readString2(record.payload.message);
|
|
18126
|
+
if (!text2 || hasDuplicateTextMessage({ messages, turnId: currentTurnId, role: "user", text: text2 }))
|
|
18127
|
+
continue;
|
|
18128
|
+
pushMessage(messages, {
|
|
18129
|
+
id: `user:${currentTurnId}:${blockIndex}`,
|
|
18130
|
+
turnId: currentTurnId,
|
|
18131
|
+
role: "user",
|
|
18132
|
+
kind: "text",
|
|
18133
|
+
text: text2,
|
|
18134
|
+
status: "completed",
|
|
18135
|
+
createdAt: record.timestamp
|
|
18136
|
+
});
|
|
18137
|
+
blockIndex += 1;
|
|
18138
|
+
continue;
|
|
18139
|
+
}
|
|
18140
|
+
if (eventType === "agent_message" && currentTurnId) {
|
|
18141
|
+
const text2 = readString2(record.payload.message);
|
|
18142
|
+
if (!text2)
|
|
18143
|
+
continue;
|
|
18144
|
+
const phase = readString2(record.payload.phase) ?? undefined;
|
|
18145
|
+
if (hasDuplicateTextMessage({ messages, turnId: currentTurnId, role: "assistant", text: text2, phase }))
|
|
18146
|
+
continue;
|
|
18147
|
+
pushMessage(messages, {
|
|
18148
|
+
id: `assistant:${currentTurnId}:${blockIndex}`,
|
|
18149
|
+
turnId: currentTurnId,
|
|
18150
|
+
role: "assistant",
|
|
18151
|
+
kind: phase === "analysis" ? "thinking" : "text",
|
|
18152
|
+
...phase ? { phase } : {},
|
|
18153
|
+
text: text2,
|
|
18154
|
+
status: "completed",
|
|
18155
|
+
createdAt: record.timestamp
|
|
18156
|
+
});
|
|
18157
|
+
blockIndex += 1;
|
|
18158
|
+
}
|
|
18159
|
+
continue;
|
|
18160
|
+
}
|
|
18161
|
+
if (record.type !== "response_item" || !currentTurnId)
|
|
18162
|
+
continue;
|
|
18163
|
+
const payloadType = readString2(record.payload.type);
|
|
18164
|
+
if (payloadType === "reasoning") {
|
|
18165
|
+
const summary = readReasoningSummary(record.payload.summary);
|
|
18166
|
+
if (summary.length === 0)
|
|
18167
|
+
continue;
|
|
18168
|
+
pushMessage(messages, {
|
|
18169
|
+
id: `reasoning:${currentTurnId}:${blockIndex}`,
|
|
18170
|
+
turnId: currentTurnId,
|
|
18171
|
+
role: "assistant",
|
|
18172
|
+
kind: "thinking",
|
|
18173
|
+
phase: "analysis",
|
|
18174
|
+
text: summary,
|
|
18175
|
+
status: "completed",
|
|
18176
|
+
createdAt: record.timestamp
|
|
18177
|
+
});
|
|
18178
|
+
blockIndex += 1;
|
|
18179
|
+
continue;
|
|
18180
|
+
}
|
|
18181
|
+
if (payloadType === "function_call" || payloadType === "custom_tool_call") {
|
|
18182
|
+
const callId = readString2(record.payload.call_id);
|
|
18183
|
+
if (!callId)
|
|
18184
|
+
continue;
|
|
18185
|
+
const toolName = readString2(record.payload.name) ?? "tool";
|
|
18186
|
+
const argumentsText = payloadType === "custom_tool_call" ? typeof record.payload.input === "string" ? record.payload.input : compactJson2(record.payload.input ?? {}) : typeof record.payload.arguments === "string" ? record.payload.arguments : compactJson2(record.payload.arguments ?? {});
|
|
18187
|
+
const command = readToolCommand(toolName, argumentsText);
|
|
18188
|
+
const cwd = payloadType === "custom_tool_call" ? null : readToolCwd(argumentsText);
|
|
18189
|
+
toolCallMetadata.set(callId, {
|
|
18190
|
+
toolName,
|
|
18191
|
+
...command ? { command } : {},
|
|
18192
|
+
...cwd ? { cwd } : {}
|
|
18193
|
+
});
|
|
18194
|
+
pushMessage(messages, {
|
|
18195
|
+
id: callId,
|
|
18196
|
+
turnId: currentTurnId,
|
|
18197
|
+
role: "assistant",
|
|
18198
|
+
kind: "toolUse",
|
|
18199
|
+
toolName,
|
|
18200
|
+
toolCallId: callId,
|
|
18201
|
+
text: payloadType === "custom_tool_call" ? toolName : buildToolUseText(toolName, argumentsText),
|
|
18202
|
+
...command ? { command } : {},
|
|
18203
|
+
...cwd ? { cwd } : {},
|
|
18204
|
+
status: record.payload.status === "failed" ? "failed" : "completed",
|
|
18205
|
+
createdAt: record.timestamp
|
|
18206
|
+
});
|
|
18207
|
+
blockIndex += 1;
|
|
18208
|
+
continue;
|
|
18209
|
+
}
|
|
18210
|
+
if (payloadType === "function_call_output" || payloadType === "custom_tool_call_output") {
|
|
18211
|
+
const callId = readString2(record.payload.call_id);
|
|
18212
|
+
if (!callId)
|
|
18213
|
+
continue;
|
|
18214
|
+
const metadata = toolCallMetadata.get(callId);
|
|
18215
|
+
const output = typeof record.payload.output === "string" ? record.payload.output.trimEnd() : compactJson2(record.payload.output ?? "");
|
|
18216
|
+
const exitCode = readOutputExitCode(output);
|
|
18217
|
+
pushMessage(messages, {
|
|
18218
|
+
id: `${callId}:result`,
|
|
18219
|
+
turnId: currentTurnId,
|
|
18220
|
+
role: "user",
|
|
18221
|
+
kind: "toolResult",
|
|
18222
|
+
...metadata?.toolName ? { toolName: metadata.toolName } : {},
|
|
18223
|
+
toolCallId: callId,
|
|
18224
|
+
text: truncate2(output),
|
|
18225
|
+
...metadata?.command ? { command: metadata.command } : {},
|
|
18226
|
+
...metadata?.cwd ? { cwd: metadata.cwd } : {},
|
|
18227
|
+
status: readOutputStatus(output),
|
|
18228
|
+
createdAt: record.timestamp,
|
|
18229
|
+
exitCode
|
|
18230
|
+
});
|
|
18231
|
+
blockIndex += 1;
|
|
18232
|
+
}
|
|
18233
|
+
}
|
|
18234
|
+
return finalizeToolStatuses(messages);
|
|
18235
|
+
}
|
|
18236
|
+
async function readCodexSessionMessages(thread) {
|
|
18237
|
+
if (!thread.path)
|
|
18238
|
+
return [];
|
|
18239
|
+
try {
|
|
18240
|
+
const file = Bun.file(thread.path);
|
|
18241
|
+
if (!await file.exists())
|
|
18242
|
+
return [];
|
|
18243
|
+
return parseCodexSessionMessages(await file.text());
|
|
18244
|
+
} catch {
|
|
18245
|
+
return [];
|
|
18246
|
+
}
|
|
18247
|
+
}
|
|
18248
|
+
|
|
18249
|
+
// backend/src/services/worktree-conversation-result.ts
|
|
18250
|
+
function ok(data) {
|
|
18251
|
+
return { ok: true, data };
|
|
18252
|
+
}
|
|
18253
|
+
function err(status, error) {
|
|
18254
|
+
return { ok: false, status, error };
|
|
18255
|
+
}
|
|
18256
|
+
|
|
18257
|
+
// backend/src/services/worktree-conversation-service.ts
|
|
18258
|
+
function resolveCodexAppServerLaunchContext(input) {
|
|
18259
|
+
if (input.worktree.agentName !== "codex" || input.meta.agent !== "codex") {
|
|
18260
|
+
return err(409, "Codex web chat is only available for Codex worktrees");
|
|
18261
|
+
}
|
|
18262
|
+
if (!input.profile) {
|
|
18263
|
+
return err(409, `Profile is missing for Codex web chat: ${input.meta.profile}`);
|
|
18264
|
+
}
|
|
18265
|
+
if (input.meta.runtime !== "host" || input.profile.runtime !== "host") {
|
|
18266
|
+
return err(409, "Codex web chat is only available for host-runtime worktrees. Use the terminal for Docker worktrees.");
|
|
18267
|
+
}
|
|
18268
|
+
if (input.profile.yolo !== true) {
|
|
18269
|
+
return err(409, "Codex web chat requires a yolo profile to preserve the Codex approval policy. Use the terminal for this worktree.");
|
|
18270
|
+
}
|
|
18271
|
+
return ok({
|
|
18272
|
+
approvalPolicy: "never",
|
|
18273
|
+
personality: "pragmatic",
|
|
18274
|
+
sandbox: "danger-full-access"
|
|
18275
|
+
});
|
|
18276
|
+
}
|
|
18277
|
+
function isCodexWorktree(worktree) {
|
|
18278
|
+
return worktree.agentName === "codex";
|
|
18279
|
+
}
|
|
18280
|
+
function isCodexConversationMeta(meta) {
|
|
18281
|
+
return meta?.provider === "codexAppServer";
|
|
18282
|
+
}
|
|
18283
|
+
function toIsoTimestamp(epochSeconds) {
|
|
18284
|
+
if (epochSeconds === null)
|
|
18285
|
+
return null;
|
|
18286
|
+
return new Date(epochSeconds * 1000).toISOString();
|
|
18287
|
+
}
|
|
18288
|
+
function isUserMessageItem(item) {
|
|
18289
|
+
return item.type === "userMessage";
|
|
18290
|
+
}
|
|
18291
|
+
function isAgentMessageItem(item) {
|
|
18292
|
+
return item.type === "agentMessage";
|
|
18293
|
+
}
|
|
18294
|
+
function isCommandExecutionItem(item) {
|
|
18295
|
+
return item.type === "commandExecution";
|
|
18296
|
+
}
|
|
18297
|
+
function isFileChangeItem(item) {
|
|
18298
|
+
return item.type === "fileChange";
|
|
18299
|
+
}
|
|
18300
|
+
function isMcpToolCallItem(item) {
|
|
18301
|
+
return item.type === "mcpToolCall";
|
|
18302
|
+
}
|
|
18303
|
+
function isDynamicToolCallItem(item) {
|
|
18304
|
+
return item.type === "dynamicToolCall";
|
|
18305
|
+
}
|
|
18306
|
+
function isWebSearchItem(item) {
|
|
18307
|
+
return item.type === "webSearch";
|
|
18308
|
+
}
|
|
18309
|
+
function extractUserText(item) {
|
|
18310
|
+
return item.content.map((contentItem) => contentItem.text ?? "").join("").trim();
|
|
18311
|
+
}
|
|
18312
|
+
function extractAgentText(item) {
|
|
18313
|
+
return item.text ?? item.message ?? "";
|
|
18314
|
+
}
|
|
18315
|
+
function commandExecutionStatus(item) {
|
|
18316
|
+
switch (item.status) {
|
|
18317
|
+
case "inProgress":
|
|
18318
|
+
return "inProgress";
|
|
18319
|
+
case "completed":
|
|
18320
|
+
return item.exitCode !== null && item.exitCode !== 0 ? "failed" : "completed";
|
|
18321
|
+
case "failed":
|
|
18322
|
+
case "declined":
|
|
18323
|
+
return "failed";
|
|
18324
|
+
}
|
|
18325
|
+
}
|
|
18326
|
+
function commandExecutionDisplayText(item) {
|
|
18327
|
+
const commands = item.commandActions.map((action) => action.command ?? "").filter((command) => command.length > 0);
|
|
18328
|
+
return commands.length > 0 ? commands.join(" && ") : item.command;
|
|
18329
|
+
}
|
|
18330
|
+
function toolStatus(status) {
|
|
18331
|
+
switch (status) {
|
|
18332
|
+
case "inProgress":
|
|
18333
|
+
return "inProgress";
|
|
18334
|
+
case "completed":
|
|
18335
|
+
return "completed";
|
|
18336
|
+
case "failed":
|
|
18337
|
+
case "declined":
|
|
18338
|
+
return "failed";
|
|
18339
|
+
}
|
|
18340
|
+
}
|
|
18341
|
+
function jsonDisplayText(value) {
|
|
18342
|
+
return typeof value === "string" ? value : JSON.stringify(value, null, 2) ?? "";
|
|
18343
|
+
}
|
|
18344
|
+
function patchChangeLabel(change) {
|
|
18345
|
+
switch (change.kind.type) {
|
|
18346
|
+
case "add":
|
|
18347
|
+
return `add ${change.path}`;
|
|
18348
|
+
case "delete":
|
|
18349
|
+
return `delete ${change.path}`;
|
|
18350
|
+
case "update":
|
|
18351
|
+
return change.kind.move_path ? `move ${change.kind.move_path} -> ${change.path}` : `update ${change.path}`;
|
|
18352
|
+
}
|
|
18353
|
+
}
|
|
18354
|
+
function fileChangeDisplayText(item) {
|
|
18355
|
+
return item.changes.map(patchChangeLabel).join(`
|
|
18356
|
+
`);
|
|
18357
|
+
}
|
|
18358
|
+
function fileChangeResultText(item) {
|
|
18359
|
+
return item.changes.map((change) => change.diff.trimEnd()).filter((diff) => diff.length > 0).join(`
|
|
18360
|
+
|
|
18361
|
+
`);
|
|
18362
|
+
}
|
|
18363
|
+
function mcpContentText(content) {
|
|
18364
|
+
if (isRecord3(content) && typeof content.text === "string")
|
|
18365
|
+
return content.text;
|
|
18366
|
+
return jsonDisplayText(content);
|
|
18367
|
+
}
|
|
18368
|
+
function mcpToolResultText(item) {
|
|
18369
|
+
if (item.error)
|
|
18370
|
+
return item.error.message;
|
|
18371
|
+
if (!item.result)
|
|
18372
|
+
return "";
|
|
18373
|
+
const parts = item.result.content.map(mcpContentText);
|
|
18374
|
+
if (item.result.structuredContent !== null) {
|
|
18375
|
+
parts.push(jsonDisplayText(item.result.structuredContent));
|
|
18376
|
+
}
|
|
18377
|
+
return parts.join(`
|
|
18378
|
+
|
|
18379
|
+
`).trim();
|
|
18380
|
+
}
|
|
18381
|
+
function dynamicToolName(item) {
|
|
18382
|
+
return item.namespace ? `${item.namespace}.${item.tool}` : item.tool;
|
|
18383
|
+
}
|
|
18384
|
+
function dynamicToolContentText(content) {
|
|
18385
|
+
switch (content.type) {
|
|
18386
|
+
case "inputText":
|
|
18387
|
+
return content.text;
|
|
18388
|
+
case "inputImage":
|
|
18389
|
+
return content.imageUrl;
|
|
18390
|
+
}
|
|
18391
|
+
}
|
|
18392
|
+
function dynamicToolResultText(item) {
|
|
18393
|
+
return (item.contentItems ?? []).map(dynamicToolContentText).join(`
|
|
18394
|
+
|
|
18395
|
+
`).trim();
|
|
18396
|
+
}
|
|
18397
|
+
function webSearchDisplayText(item) {
|
|
18398
|
+
const action = item.action;
|
|
18399
|
+
if (!action)
|
|
18400
|
+
return item.query;
|
|
18401
|
+
switch (action.type) {
|
|
18402
|
+
case "search":
|
|
18403
|
+
return action.queries?.join(`
|
|
18404
|
+
`) ?? action.query ?? item.query;
|
|
18405
|
+
case "openPage":
|
|
18406
|
+
return action.url ?? item.query;
|
|
18407
|
+
case "findInPage":
|
|
18408
|
+
return [action.url, action.pattern].filter((part) => part !== null).join(`
|
|
18409
|
+
`);
|
|
18410
|
+
case "other":
|
|
18411
|
+
return item.query;
|
|
18412
|
+
}
|
|
18413
|
+
}
|
|
18414
|
+
function isActiveTurnStatus(status) {
|
|
18415
|
+
return status === "inProgress" || status === "active" || status === "running" || status === "pending" || status === "queued";
|
|
18416
|
+
}
|
|
18417
|
+
function findActiveTurn(thread) {
|
|
18418
|
+
for (let index = thread.turns.length - 1;index >= 0; index -= 1) {
|
|
18419
|
+
const turn = thread.turns[index];
|
|
18420
|
+
if (isActiveTurnStatus(turn.status))
|
|
18421
|
+
return turn;
|
|
18422
|
+
}
|
|
18423
|
+
return null;
|
|
18424
|
+
}
|
|
18425
|
+
function buildCommandExecutionMessages(input) {
|
|
18426
|
+
const { item, turnId, createdAt, order } = input;
|
|
18427
|
+
const status = commandExecutionStatus(item);
|
|
18428
|
+
const toolUse = {
|
|
18429
|
+
id: item.id,
|
|
18430
|
+
turnId,
|
|
18431
|
+
order,
|
|
18432
|
+
role: "assistant",
|
|
18433
|
+
kind: "toolUse",
|
|
18434
|
+
toolName: "shell",
|
|
18435
|
+
toolCallId: item.id,
|
|
18436
|
+
text: commandExecutionDisplayText(item),
|
|
18437
|
+
command: item.command,
|
|
18438
|
+
cwd: item.cwd ?? undefined,
|
|
18439
|
+
status,
|
|
18440
|
+
createdAt,
|
|
18441
|
+
exitCode: item.exitCode,
|
|
18442
|
+
durationMs: item.durationMs
|
|
18443
|
+
};
|
|
18444
|
+
const output = item.aggregatedOutput?.trimEnd() ?? "";
|
|
18445
|
+
if (output.length === 0)
|
|
18446
|
+
return [toolUse];
|
|
18447
|
+
return [
|
|
18448
|
+
toolUse,
|
|
18449
|
+
{
|
|
18450
|
+
id: `${item.id}:result`,
|
|
18451
|
+
turnId,
|
|
18452
|
+
order: order + 1,
|
|
18453
|
+
role: "user",
|
|
18454
|
+
kind: "toolResult",
|
|
18455
|
+
toolName: "shell",
|
|
18456
|
+
toolCallId: item.id,
|
|
18457
|
+
text: output,
|
|
18458
|
+
command: item.command,
|
|
18459
|
+
cwd: item.cwd ?? undefined,
|
|
18460
|
+
status,
|
|
18461
|
+
createdAt,
|
|
18462
|
+
exitCode: item.exitCode,
|
|
18463
|
+
durationMs: item.durationMs
|
|
18464
|
+
}
|
|
18465
|
+
];
|
|
18466
|
+
}
|
|
18467
|
+
function buildFileChangeMessages(input) {
|
|
18468
|
+
const { item, turnId, createdAt, order } = input;
|
|
18469
|
+
const status = toolStatus(item.status);
|
|
18470
|
+
const toolUse = {
|
|
18471
|
+
id: item.id,
|
|
18472
|
+
turnId,
|
|
18473
|
+
order,
|
|
18474
|
+
role: "assistant",
|
|
18475
|
+
kind: "toolUse",
|
|
18476
|
+
toolName: "file change",
|
|
18477
|
+
toolCallId: item.id,
|
|
18478
|
+
text: fileChangeDisplayText(item),
|
|
18479
|
+
status,
|
|
18480
|
+
createdAt
|
|
18481
|
+
};
|
|
18482
|
+
const resultText = fileChangeResultText(item);
|
|
18483
|
+
if (resultText.length === 0)
|
|
18484
|
+
return [toolUse];
|
|
18485
|
+
return [
|
|
18486
|
+
toolUse,
|
|
18487
|
+
{
|
|
18488
|
+
id: `${item.id}:result`,
|
|
18489
|
+
turnId,
|
|
18490
|
+
order: order + 1,
|
|
18491
|
+
role: "user",
|
|
18492
|
+
kind: "toolResult",
|
|
18493
|
+
toolName: "file change",
|
|
18494
|
+
toolCallId: item.id,
|
|
18495
|
+
text: resultText,
|
|
18496
|
+
status,
|
|
18497
|
+
createdAt
|
|
18498
|
+
}
|
|
18499
|
+
];
|
|
18500
|
+
}
|
|
18501
|
+
function buildMcpToolCallMessages(input) {
|
|
18502
|
+
const { item, turnId, createdAt, order } = input;
|
|
18503
|
+
const status = item.error ? "failed" : toolStatus(item.status);
|
|
18504
|
+
const toolName = `${item.server}.${item.tool}`;
|
|
18505
|
+
const toolUse = {
|
|
18506
|
+
id: item.id,
|
|
18507
|
+
turnId,
|
|
18508
|
+
order,
|
|
18509
|
+
role: "assistant",
|
|
18510
|
+
kind: "toolUse",
|
|
18511
|
+
toolName,
|
|
18512
|
+
toolCallId: item.id,
|
|
18513
|
+
text: jsonDisplayText(item.arguments),
|
|
18514
|
+
status,
|
|
18515
|
+
createdAt,
|
|
18516
|
+
durationMs: item.durationMs
|
|
18517
|
+
};
|
|
18518
|
+
const resultText = mcpToolResultText(item);
|
|
18519
|
+
if (resultText.length === 0)
|
|
18520
|
+
return [toolUse];
|
|
18521
|
+
return [
|
|
18522
|
+
toolUse,
|
|
18523
|
+
{
|
|
18524
|
+
id: `${item.id}:result`,
|
|
18525
|
+
turnId,
|
|
18526
|
+
order: order + 1,
|
|
18527
|
+
role: "user",
|
|
18528
|
+
kind: "toolResult",
|
|
18529
|
+
toolName,
|
|
18530
|
+
toolCallId: item.id,
|
|
18531
|
+
text: resultText,
|
|
18532
|
+
status,
|
|
18533
|
+
createdAt,
|
|
18534
|
+
durationMs: item.durationMs
|
|
18535
|
+
}
|
|
18536
|
+
];
|
|
18537
|
+
}
|
|
18538
|
+
function buildDynamicToolCallMessages(input) {
|
|
18539
|
+
const { item, turnId, createdAt, order } = input;
|
|
18540
|
+
const status = item.success === false ? "failed" : toolStatus(item.status);
|
|
18541
|
+
const toolName = dynamicToolName(item);
|
|
18542
|
+
const toolUse = {
|
|
18543
|
+
id: item.id,
|
|
18544
|
+
turnId,
|
|
18545
|
+
order,
|
|
18546
|
+
role: "assistant",
|
|
18547
|
+
kind: "toolUse",
|
|
18548
|
+
toolName,
|
|
18549
|
+
toolCallId: item.id,
|
|
18550
|
+
text: jsonDisplayText(item.arguments),
|
|
18551
|
+
status,
|
|
18552
|
+
createdAt,
|
|
18553
|
+
durationMs: item.durationMs
|
|
18554
|
+
};
|
|
18555
|
+
const resultText = dynamicToolResultText(item);
|
|
18556
|
+
if (resultText.length === 0)
|
|
18557
|
+
return [toolUse];
|
|
18558
|
+
return [
|
|
18559
|
+
toolUse,
|
|
18560
|
+
{
|
|
18561
|
+
id: `${item.id}:result`,
|
|
18562
|
+
turnId,
|
|
18563
|
+
order: order + 1,
|
|
18564
|
+
role: "user",
|
|
18565
|
+
kind: "toolResult",
|
|
18566
|
+
toolName,
|
|
18567
|
+
toolCallId: item.id,
|
|
18568
|
+
text: resultText,
|
|
18569
|
+
status,
|
|
18570
|
+
createdAt,
|
|
18571
|
+
durationMs: item.durationMs
|
|
18572
|
+
}
|
|
18573
|
+
];
|
|
18574
|
+
}
|
|
18575
|
+
function buildWebSearchMessages(input) {
|
|
18576
|
+
const { item, turnId, createdAt, order } = input;
|
|
18577
|
+
return [{
|
|
18578
|
+
id: item.id,
|
|
18579
|
+
turnId,
|
|
18580
|
+
order,
|
|
18581
|
+
role: "assistant",
|
|
18582
|
+
kind: "toolUse",
|
|
18583
|
+
toolName: "web search",
|
|
18584
|
+
toolCallId: item.id,
|
|
18585
|
+
text: webSearchDisplayText(item),
|
|
18586
|
+
status: "completed",
|
|
18587
|
+
createdAt
|
|
18588
|
+
}];
|
|
18589
|
+
}
|
|
18590
|
+
function buildCodexItemConversationMessages(input) {
|
|
18591
|
+
const { item, turnId, turnStatus, createdAt, order, includeEmptyText = false } = input;
|
|
18592
|
+
if (isUserMessageItem(item)) {
|
|
18593
|
+
const text = extractUserText(item);
|
|
18594
|
+
if (text.length === 0 && !includeEmptyText)
|
|
18595
|
+
return [];
|
|
18596
|
+
return [{
|
|
18597
|
+
id: item.id,
|
|
18598
|
+
turnId,
|
|
18599
|
+
order,
|
|
18600
|
+
role: "user",
|
|
18601
|
+
kind: "text",
|
|
18602
|
+
text,
|
|
18603
|
+
status: "completed",
|
|
18604
|
+
createdAt
|
|
18605
|
+
}];
|
|
18606
|
+
}
|
|
18607
|
+
if (isAgentMessageItem(item)) {
|
|
18608
|
+
const text = extractAgentText(item);
|
|
18609
|
+
if (text.length === 0 && !includeEmptyText)
|
|
18610
|
+
return [];
|
|
18611
|
+
const phase = item.phase ?? undefined;
|
|
18612
|
+
const isThinking = phase === "analysis";
|
|
18613
|
+
return [{
|
|
18614
|
+
id: item.id,
|
|
18615
|
+
turnId,
|
|
18616
|
+
order,
|
|
18617
|
+
role: "assistant",
|
|
18618
|
+
kind: isThinking ? "thinking" : "text",
|
|
18619
|
+
phase,
|
|
18620
|
+
text,
|
|
18621
|
+
status: isActiveTurnStatus(turnStatus) ? "inProgress" : "completed",
|
|
18622
|
+
createdAt
|
|
18623
|
+
}];
|
|
18624
|
+
}
|
|
18625
|
+
if (isCommandExecutionItem(item))
|
|
18626
|
+
return buildCommandExecutionMessages({ item, turnId, createdAt, order });
|
|
18627
|
+
if (isFileChangeItem(item))
|
|
18628
|
+
return buildFileChangeMessages({ item, turnId, createdAt, order });
|
|
18629
|
+
if (isMcpToolCallItem(item))
|
|
18630
|
+
return buildMcpToolCallMessages({ item, turnId, createdAt, order });
|
|
18631
|
+
if (isDynamicToolCallItem(item))
|
|
18632
|
+
return buildDynamicToolCallMessages({ item, turnId, createdAt, order });
|
|
18633
|
+
if (isWebSearchItem(item))
|
|
18634
|
+
return buildWebSearchMessages({ item, turnId, createdAt, order });
|
|
18635
|
+
return [];
|
|
18636
|
+
}
|
|
18637
|
+
function buildConversationMessages(thread) {
|
|
18638
|
+
const messages = [];
|
|
18639
|
+
let order = 0;
|
|
18640
|
+
for (const turn of thread.turns) {
|
|
18641
|
+
for (const item of turn.items) {
|
|
18642
|
+
const itemMessages = buildCodexItemConversationMessages({
|
|
18643
|
+
item,
|
|
18644
|
+
turnId: turn.id,
|
|
18645
|
+
turnStatus: turn.status,
|
|
18646
|
+
createdAt: toIsoTimestamp(isUserMessageItem(item) ? turn.startedAt : turn.completedAt ?? turn.startedAt),
|
|
18647
|
+
order
|
|
18648
|
+
});
|
|
18649
|
+
messages.push(...itemMessages);
|
|
18650
|
+
order += itemMessages.length;
|
|
18651
|
+
}
|
|
18652
|
+
}
|
|
18653
|
+
return messages;
|
|
18654
|
+
}
|
|
18655
|
+
function buildConversationState(thread, sessionMessages = []) {
|
|
18656
|
+
const activeTurn = findActiveTurn(thread);
|
|
18657
|
+
const messages = sessionMessages.length > 0 ? sessionMessages : buildConversationMessages(thread);
|
|
18658
|
+
return {
|
|
18659
|
+
provider: "codexAppServer",
|
|
18660
|
+
conversationId: thread.id,
|
|
18661
|
+
cwd: thread.cwd,
|
|
18662
|
+
running: thread.status.type === "active" || activeTurn !== null,
|
|
18663
|
+
activeTurnId: activeTurn?.id ?? null,
|
|
18664
|
+
messages
|
|
18665
|
+
};
|
|
18666
|
+
}
|
|
18667
|
+
function selectDiscoveredThread(threads) {
|
|
18668
|
+
if (threads.length === 0)
|
|
18669
|
+
return null;
|
|
18670
|
+
return [...threads].sort((left, right) => right.updatedAt - left.updatedAt)[0] ?? null;
|
|
18671
|
+
}
|
|
18672
|
+
function buildConversationMeta(thread, now) {
|
|
18673
|
+
return {
|
|
18674
|
+
provider: "codexAppServer",
|
|
18675
|
+
conversationId: thread.id,
|
|
18676
|
+
threadId: thread.id,
|
|
18677
|
+
cwd: thread.cwd,
|
|
18678
|
+
lastSeenAt: now.toISOString()
|
|
18679
|
+
};
|
|
18680
|
+
}
|
|
18681
|
+
function sameConversationMeta(left, right) {
|
|
18682
|
+
return left?.provider === right.provider && left.conversationId === right.conversationId && left.cwd === right.cwd;
|
|
18683
|
+
}
|
|
18684
|
+
function toWorktreeConversationResponse(worktree, conversationMeta, thread, sessionMessages) {
|
|
18685
|
+
return {
|
|
18686
|
+
worktree: buildAgentsUiWorktreeSummary(worktree, conversationMeta),
|
|
18687
|
+
conversation: buildConversationState(thread, sessionMessages)
|
|
18688
|
+
};
|
|
18689
|
+
}
|
|
18690
|
+
|
|
18691
|
+
class WorktreeConversationService {
|
|
18692
|
+
deps;
|
|
18693
|
+
now;
|
|
18694
|
+
readSessionMessages;
|
|
18695
|
+
readMeta;
|
|
18696
|
+
writeMeta;
|
|
18697
|
+
constructor(deps) {
|
|
18698
|
+
this.deps = deps;
|
|
18699
|
+
this.now = deps.now ?? (() => new Date);
|
|
18700
|
+
this.readSessionMessages = deps.readSessionMessages ?? readCodexSessionMessages;
|
|
18701
|
+
this.readMeta = deps.readMeta ?? readWorktreeMeta;
|
|
18702
|
+
this.writeMeta = deps.writeMeta ?? writeWorktreeMeta;
|
|
18703
|
+
}
|
|
18704
|
+
async attachWorktreeConversation(worktree) {
|
|
18705
|
+
return await this.withResolvedConversation(worktree, true, async ({ conversationMeta, thread }) => {
|
|
18706
|
+
const sessionMessages = await this.readSessionMessages(thread);
|
|
18707
|
+
return ok(toWorktreeConversationResponse(worktree, conversationMeta, thread, sessionMessages));
|
|
18708
|
+
});
|
|
18709
|
+
}
|
|
18710
|
+
async readWorktreeConversation(worktree) {
|
|
18711
|
+
return await this.withResolvedConversation(worktree, false, async ({ conversationMeta, thread }) => {
|
|
18712
|
+
const sessionMessages = await this.readSessionMessages(thread);
|
|
18713
|
+
return ok(toWorktreeConversationResponse(worktree, conversationMeta, thread, sessionMessages));
|
|
18714
|
+
});
|
|
18715
|
+
}
|
|
18716
|
+
async sendWorktreeConversationMessage(worktree, text) {
|
|
18717
|
+
return await this.withResolvedConversation(worktree, true, async ({ thread, launchContext }) => {
|
|
18718
|
+
const started = await this.deps.appServer.turnStart({
|
|
18719
|
+
threadId: thread.id,
|
|
18720
|
+
cwd: worktree.path,
|
|
18721
|
+
approvalPolicy: launchContext.approvalPolicy,
|
|
18722
|
+
input: [{ type: "text", text }]
|
|
18723
|
+
});
|
|
18724
|
+
return ok({
|
|
18725
|
+
conversationId: thread.id,
|
|
18726
|
+
turnId: started.turn.id,
|
|
18727
|
+
running: true
|
|
18728
|
+
});
|
|
18729
|
+
});
|
|
18730
|
+
}
|
|
18731
|
+
async interruptWorktreeConversation(worktree) {
|
|
18732
|
+
return await this.withResolvedConversation(worktree, false, async ({ thread }) => {
|
|
18733
|
+
const conversation = buildConversationState(thread);
|
|
18734
|
+
const turnId = conversation.activeTurnId;
|
|
18735
|
+
if (!turnId) {
|
|
18736
|
+
return err(409, "No active Codex turn to interrupt");
|
|
18737
|
+
}
|
|
18738
|
+
await this.deps.appServer.turnInterrupt({
|
|
18739
|
+
threadId: thread.id,
|
|
18740
|
+
turnId
|
|
18741
|
+
});
|
|
18742
|
+
return ok({
|
|
18743
|
+
conversationId: thread.id,
|
|
18744
|
+
turnId,
|
|
18745
|
+
interrupted: true
|
|
18746
|
+
});
|
|
18747
|
+
});
|
|
18748
|
+
}
|
|
18749
|
+
async withResolvedConversation(worktree, allowCreate, fn) {
|
|
18750
|
+
if (!isCodexWorktree(worktree)) {
|
|
18751
|
+
return err(409, "Worktree chat is only available for Codex worktrees");
|
|
18752
|
+
}
|
|
18753
|
+
try {
|
|
18754
|
+
const resolved = await this.resolveConversation(worktree, allowCreate);
|
|
18755
|
+
if (!resolved.ok)
|
|
18756
|
+
return resolved;
|
|
18757
|
+
return await fn(resolved.data);
|
|
18758
|
+
} catch (error) {
|
|
18759
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18760
|
+
return err(502, message);
|
|
18761
|
+
}
|
|
18762
|
+
}
|
|
18763
|
+
async resolveConversation(worktree, allowCreate) {
|
|
18764
|
+
const gitDir = this.deps.git.resolveWorktreeGitDir(worktree.path);
|
|
18765
|
+
const meta = await this.readMeta(gitDir);
|
|
18766
|
+
if (!meta) {
|
|
18767
|
+
return err(409, "Worktree metadata is missing");
|
|
18768
|
+
}
|
|
18769
|
+
const launchContextResult = await this.deps.resolveLaunchContext({ worktree, meta });
|
|
18770
|
+
if (!launchContextResult.ok)
|
|
18771
|
+
return launchContextResult;
|
|
18772
|
+
const launchContext = launchContextResult.data;
|
|
18773
|
+
const now = this.now();
|
|
18774
|
+
const thread = await this.resolveThread(meta, worktree.path, allowCreate, launchContext);
|
|
18775
|
+
if (!thread) {
|
|
18776
|
+
return err(404, "No Codex thread could be resolved for this worktree");
|
|
18777
|
+
}
|
|
18778
|
+
const conversationMeta = buildConversationMeta(thread, now);
|
|
18779
|
+
const nextMeta = sameConversationMeta(meta.conversation, conversationMeta) ? { ...meta, conversation: { ...conversationMeta, lastSeenAt: meta.conversation?.lastSeenAt ?? conversationMeta.lastSeenAt } } : { ...meta, conversation: conversationMeta };
|
|
18780
|
+
if (!sameConversationMeta(meta.conversation, conversationMeta)) {
|
|
18781
|
+
await this.writeMeta(gitDir, nextMeta);
|
|
18782
|
+
}
|
|
18783
|
+
return ok({
|
|
18784
|
+
gitDir,
|
|
18785
|
+
meta: nextMeta,
|
|
18786
|
+
thread,
|
|
18787
|
+
conversationMeta: nextMeta.conversation ?? conversationMeta,
|
|
18788
|
+
launchContext
|
|
18789
|
+
});
|
|
18790
|
+
}
|
|
18791
|
+
async resolveThread(meta, cwd, allowCreate, launchContext) {
|
|
18792
|
+
const savedThreadId = isCodexConversationMeta(meta.conversation) ? meta.conversation.threadId : null;
|
|
18793
|
+
if (savedThreadId) {
|
|
18794
|
+
const savedThread = await this.tryLoadThread(savedThreadId, cwd, launchContext);
|
|
18795
|
+
if (savedThread)
|
|
18796
|
+
return savedThread;
|
|
18797
|
+
log.warn(`[agents] saved codex thread missing, starting fresh conversation cwd=${cwd} threadId=${savedThreadId}`);
|
|
18798
|
+
} else {
|
|
18799
|
+
const discoveredThread = selectDiscoveredThread((await this.deps.appServer.threadList({
|
|
18800
|
+
cwd,
|
|
18801
|
+
limit: 20,
|
|
18802
|
+
sortKey: "updated_at"
|
|
18803
|
+
})).data);
|
|
18804
|
+
if (discoveredThread) {
|
|
18805
|
+
return await this.ensureThreadLoaded(discoveredThread.id, cwd, launchContext);
|
|
18806
|
+
}
|
|
18807
|
+
}
|
|
18808
|
+
if (!allowCreate)
|
|
18809
|
+
return null;
|
|
18810
|
+
const started = await this.deps.appServer.threadStart({
|
|
18811
|
+
cwd,
|
|
18812
|
+
approvalPolicy: launchContext.approvalPolicy,
|
|
18813
|
+
personality: launchContext.personality,
|
|
18814
|
+
sandbox: launchContext.sandbox
|
|
18815
|
+
});
|
|
18816
|
+
return started.thread;
|
|
18817
|
+
}
|
|
18818
|
+
async tryLoadThread(threadId, cwd, launchContext) {
|
|
18819
|
+
try {
|
|
18820
|
+
return await this.ensureThreadLoaded(threadId, cwd, launchContext);
|
|
18821
|
+
} catch {
|
|
18822
|
+
return null;
|
|
18823
|
+
}
|
|
18824
|
+
}
|
|
18825
|
+
async ensureThreadLoaded(threadId, cwd, launchContext) {
|
|
18826
|
+
const initial = await this.deps.appServer.threadRead(threadId, false);
|
|
18827
|
+
if (initial.thread.status.type === "notLoaded") {
|
|
18828
|
+
await this.deps.appServer.threadResume({
|
|
18829
|
+
threadId,
|
|
18830
|
+
cwd,
|
|
18831
|
+
approvalPolicy: launchContext.approvalPolicy,
|
|
18832
|
+
personality: launchContext.personality,
|
|
18833
|
+
sandbox: launchContext.sandbox
|
|
18834
|
+
});
|
|
18835
|
+
}
|
|
18836
|
+
return (await this.deps.appServer.threadRead(threadId, true)).thread;
|
|
18837
|
+
}
|
|
18838
|
+
}
|
|
18839
|
+
|
|
18840
|
+
// backend/src/services/agents-ui-stream-service.ts
|
|
18841
|
+
function readNotificationParams(raw) {
|
|
18842
|
+
return isRecord3(raw) ? raw : null;
|
|
18843
|
+
}
|
|
18844
|
+
function readThreadId(raw) {
|
|
18845
|
+
return typeof raw === "string" && raw.length > 0 ? raw : null;
|
|
18846
|
+
}
|
|
18847
|
+
function readStatusType(raw) {
|
|
18848
|
+
if (typeof raw === "string")
|
|
18849
|
+
return raw;
|
|
18850
|
+
if (!isRecord3(raw))
|
|
18851
|
+
return null;
|
|
18852
|
+
return typeof raw.type === "string" ? raw.type : null;
|
|
18853
|
+
}
|
|
18854
|
+
function readNotificationStatusType(notification) {
|
|
18855
|
+
const params = readNotificationParams(notification.params);
|
|
18856
|
+
if (!params)
|
|
18857
|
+
return null;
|
|
18858
|
+
return readStatusType(params.status) ?? (isRecord3(params.thread) ? readStatusType(params.thread.status) : null);
|
|
18859
|
+
}
|
|
18860
|
+
function isTerminalThreadStatus(statusType) {
|
|
18861
|
+
return statusType === "idle" || statusType === "completed" || statusType === "interrupted" || statusType === "failed" || statusType === "systemError";
|
|
18862
|
+
}
|
|
18863
|
+
function readNumber(raw) {
|
|
18864
|
+
return typeof raw === "number" ? raw : null;
|
|
18865
|
+
}
|
|
18866
|
+
function toIsoTimestampMs(epochMs) {
|
|
18867
|
+
if (epochMs === null)
|
|
18868
|
+
return null;
|
|
18869
|
+
return new Date(epochMs).toISOString();
|
|
18870
|
+
}
|
|
18871
|
+
function readNotificationItem(notification) {
|
|
18872
|
+
const params = readNotificationParams(notification.params);
|
|
18873
|
+
if (!params)
|
|
18874
|
+
return null;
|
|
18875
|
+
return parseCodexAppServerThreadItem(params.item);
|
|
18876
|
+
}
|
|
18877
|
+
function orderSpanForItem(item) {
|
|
18878
|
+
switch (item.type) {
|
|
18879
|
+
case "userMessage":
|
|
18880
|
+
case "agentMessage":
|
|
18881
|
+
case "webSearch":
|
|
18882
|
+
return 1;
|
|
18883
|
+
case "commandExecution":
|
|
18884
|
+
case "fileChange":
|
|
18885
|
+
case "mcpToolCall":
|
|
18886
|
+
case "dynamicToolCall":
|
|
18887
|
+
return 2;
|
|
18888
|
+
default:
|
|
18889
|
+
return null;
|
|
18890
|
+
}
|
|
18891
|
+
}
|
|
18892
|
+
function readAgentsNotificationThreadId(notification) {
|
|
18893
|
+
const params = readNotificationParams(notification.params);
|
|
18894
|
+
if (!params)
|
|
18895
|
+
return null;
|
|
18896
|
+
return readThreadId(params.threadId);
|
|
18897
|
+
}
|
|
18898
|
+
function buildAgentsUiMessageDeltaEvent(notification, order) {
|
|
18899
|
+
if (notification.method !== "item/agentMessage/delta")
|
|
18900
|
+
return null;
|
|
18901
|
+
const params = readNotificationParams(notification.params);
|
|
18902
|
+
if (!params)
|
|
18903
|
+
return null;
|
|
18904
|
+
const threadId = readThreadId(params.threadId);
|
|
18905
|
+
const turnId = readThreadId(params.turnId);
|
|
18906
|
+
const itemId = readThreadId(params.itemId);
|
|
18907
|
+
const delta = typeof params.delta === "string" ? params.delta : null;
|
|
18908
|
+
if (!threadId || !turnId || !itemId || delta === null)
|
|
18909
|
+
return null;
|
|
18910
|
+
return {
|
|
18911
|
+
type: "messageDelta",
|
|
18912
|
+
conversationId: threadId,
|
|
18913
|
+
turnId,
|
|
18914
|
+
itemId,
|
|
18915
|
+
order,
|
|
18916
|
+
delta
|
|
18917
|
+
};
|
|
18918
|
+
}
|
|
18919
|
+
function buildAgentsUiMessageUpsertEvents(notification, order) {
|
|
18920
|
+
if (notification.method !== "item/started" && notification.method !== "item/completed")
|
|
18921
|
+
return [];
|
|
18922
|
+
const params = readNotificationParams(notification.params);
|
|
18923
|
+
if (!params)
|
|
18924
|
+
return [];
|
|
18925
|
+
const threadId = readThreadId(params.threadId);
|
|
18926
|
+
const turnId = readThreadId(params.turnId);
|
|
18927
|
+
if (!threadId || !turnId)
|
|
18928
|
+
return [];
|
|
18929
|
+
const item = readNotificationItem(notification);
|
|
18930
|
+
if (!item)
|
|
18931
|
+
return [];
|
|
18932
|
+
const createdAt = toIsoTimestampMs(notification.method === "item/started" ? readNumber(params.startedAtMs) : readNumber(params.completedAtMs));
|
|
18933
|
+
return buildCodexItemConversationMessages({
|
|
18934
|
+
item,
|
|
18935
|
+
turnId,
|
|
18936
|
+
turnStatus: notification.method === "item/started" ? "inProgress" : "completed",
|
|
18937
|
+
createdAt,
|
|
18938
|
+
order,
|
|
18939
|
+
includeEmptyText: true
|
|
18940
|
+
}).map((message) => ({
|
|
18941
|
+
type: "messageUpsert",
|
|
18942
|
+
conversationId: threadId,
|
|
18943
|
+
message
|
|
18944
|
+
}));
|
|
18945
|
+
}
|
|
18946
|
+
function buildAgentsUiConversationStatusEvent(notification) {
|
|
18947
|
+
if (notification.method !== "turn/started" && notification.method !== "turn/completed" && notification.method !== "thread/status/changed")
|
|
18948
|
+
return null;
|
|
18949
|
+
const params = readNotificationParams(notification.params);
|
|
18950
|
+
if (!params)
|
|
18951
|
+
return null;
|
|
18952
|
+
const conversationId = readThreadId(params.threadId);
|
|
18953
|
+
if (!conversationId)
|
|
18954
|
+
return null;
|
|
18955
|
+
if (notification.method === "thread/status/changed") {
|
|
18956
|
+
if (!isTerminalThreadStatus(readNotificationStatusType(notification)))
|
|
18957
|
+
return null;
|
|
18958
|
+
return {
|
|
18959
|
+
type: "conversationStatus",
|
|
18960
|
+
conversationId,
|
|
18961
|
+
running: false,
|
|
18962
|
+
activeTurnId: null
|
|
18963
|
+
};
|
|
18964
|
+
}
|
|
18965
|
+
if (notification.method === "turn/started") {
|
|
18966
|
+
const activeTurnId = readThreadId(params.turnId);
|
|
18967
|
+
if (!activeTurnId)
|
|
18968
|
+
return null;
|
|
18969
|
+
return {
|
|
18970
|
+
type: "conversationStatus",
|
|
18971
|
+
conversationId,
|
|
18972
|
+
running: true,
|
|
18973
|
+
activeTurnId
|
|
18974
|
+
};
|
|
18975
|
+
}
|
|
18976
|
+
return {
|
|
18977
|
+
type: "conversationStatus",
|
|
18978
|
+
conversationId,
|
|
18979
|
+
running: false,
|
|
18980
|
+
activeTurnId: null
|
|
18981
|
+
};
|
|
18982
|
+
}
|
|
18983
|
+
|
|
18984
|
+
class AgentsConversationStreamSession {
|
|
18985
|
+
deps;
|
|
18986
|
+
revision = 0;
|
|
18987
|
+
conversationId;
|
|
18988
|
+
closed = false;
|
|
18989
|
+
nextLiveOrder;
|
|
18990
|
+
itemOrders = new Map;
|
|
18991
|
+
constructor(deps) {
|
|
18992
|
+
this.deps = deps;
|
|
18993
|
+
this.conversationId = deps.conversationId;
|
|
18994
|
+
this.nextLiveOrder = deps.nextOrder;
|
|
18995
|
+
}
|
|
18996
|
+
currentConversationId() {
|
|
18997
|
+
return this.conversationId;
|
|
18998
|
+
}
|
|
18999
|
+
close() {
|
|
19000
|
+
this.closed = true;
|
|
19001
|
+
}
|
|
19002
|
+
handleNotification(notification) {
|
|
19003
|
+
if (this.closed)
|
|
19004
|
+
return;
|
|
19005
|
+
const notificationThreadId = readAgentsNotificationThreadId(notification);
|
|
19006
|
+
if (!notificationThreadId || notificationThreadId !== this.conversationId)
|
|
19007
|
+
return;
|
|
19008
|
+
const statusEvent = buildAgentsUiConversationStatusEvent(notification);
|
|
19009
|
+
if (statusEvent) {
|
|
19010
|
+
this.deps.send({
|
|
19011
|
+
...statusEvent,
|
|
19012
|
+
revision: this.nextRevision()
|
|
19013
|
+
});
|
|
19014
|
+
return;
|
|
19015
|
+
}
|
|
19016
|
+
const deltaOrder = this.orderForDeltaNotification(notification);
|
|
19017
|
+
const deltaEvent = deltaOrder === null ? null : buildAgentsUiMessageDeltaEvent(notification, deltaOrder);
|
|
19018
|
+
if (deltaEvent) {
|
|
19019
|
+
this.deps.send({
|
|
19020
|
+
...deltaEvent,
|
|
19021
|
+
revision: this.nextRevision()
|
|
19022
|
+
});
|
|
19023
|
+
return;
|
|
19024
|
+
}
|
|
19025
|
+
const upsertOrder = this.orderForUpsertNotification(notification);
|
|
19026
|
+
if (upsertOrder !== null) {
|
|
19027
|
+
for (const upsertEvent of buildAgentsUiMessageUpsertEvents(notification, upsertOrder)) {
|
|
19028
|
+
this.deps.send({
|
|
19029
|
+
...upsertEvent,
|
|
19030
|
+
revision: this.nextRevision()
|
|
19031
|
+
});
|
|
19032
|
+
}
|
|
19033
|
+
}
|
|
19034
|
+
}
|
|
19035
|
+
nextRevision() {
|
|
19036
|
+
this.revision += 1;
|
|
19037
|
+
return this.revision;
|
|
19038
|
+
}
|
|
19039
|
+
reserveOrder(itemId, span) {
|
|
19040
|
+
const existing = this.itemOrders.get(itemId);
|
|
19041
|
+
if (existing !== undefined)
|
|
19042
|
+
return existing;
|
|
19043
|
+
const order = this.nextLiveOrder;
|
|
19044
|
+
this.nextLiveOrder += span;
|
|
19045
|
+
this.itemOrders.set(itemId, order);
|
|
19046
|
+
return order;
|
|
19047
|
+
}
|
|
19048
|
+
orderForDeltaNotification(notification) {
|
|
19049
|
+
if (notification.method !== "item/agentMessage/delta")
|
|
19050
|
+
return null;
|
|
19051
|
+
const params = readNotificationParams(notification.params);
|
|
19052
|
+
if (!params)
|
|
19053
|
+
return null;
|
|
19054
|
+
const itemId = readThreadId(params.itemId);
|
|
19055
|
+
return itemId ? this.reserveOrder(itemId, 1) : null;
|
|
19056
|
+
}
|
|
19057
|
+
orderForUpsertNotification(notification) {
|
|
19058
|
+
if (notification.method !== "item/started" && notification.method !== "item/completed")
|
|
19059
|
+
return null;
|
|
19060
|
+
const params = readNotificationParams(notification.params);
|
|
19061
|
+
if (!params || !isRecord3(params.item))
|
|
19062
|
+
return null;
|
|
19063
|
+
const itemId = readThreadId(params.item.id);
|
|
19064
|
+
const item = readNotificationItem(notification);
|
|
19065
|
+
if (!itemId || !item)
|
|
19066
|
+
return null;
|
|
19067
|
+
const orderSpan = orderSpanForItem(item);
|
|
19068
|
+
return orderSpan === null ? null : this.reserveOrder(itemId, orderSpan);
|
|
19069
|
+
}
|
|
19070
|
+
}
|
|
19071
|
+
|
|
19072
|
+
// backend/src/services/agents-ui-action-service.ts
|
|
19073
|
+
function classifyAgentsTerminalWorktreeError(error) {
|
|
19074
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
19075
|
+
if (message.startsWith("No open tmux window found for worktree: ")) {
|
|
19076
|
+
return { status: 409, error: message };
|
|
19077
|
+
}
|
|
19078
|
+
if (message.startsWith("Worktree not found: ")) {
|
|
19079
|
+
return { status: 404, error: message };
|
|
19080
|
+
}
|
|
19081
|
+
return null;
|
|
19082
|
+
}
|
|
19083
|
+
|
|
19084
|
+
// backend/src/services/snapshot-service.ts
|
|
19085
|
+
function formatElapsedSince(startedAt, now) {
|
|
19086
|
+
if (!startedAt)
|
|
19087
|
+
return "";
|
|
19088
|
+
const startedMs = Date.parse(startedAt);
|
|
19089
|
+
if (Number.isNaN(startedMs))
|
|
19090
|
+
return "";
|
|
19091
|
+
const diffMs = Math.max(0, now().getTime() - startedMs);
|
|
19092
|
+
const diffMinutes = Math.floor(diffMs / 60000);
|
|
19093
|
+
if (diffMinutes < 1)
|
|
19094
|
+
return "0m";
|
|
19095
|
+
if (diffMinutes < 60)
|
|
19096
|
+
return `${diffMinutes}m`;
|
|
19097
|
+
const diffHours = Math.floor(diffMinutes / 60);
|
|
19098
|
+
if (diffHours < 24)
|
|
19099
|
+
return `${diffHours}h`;
|
|
19100
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
19101
|
+
return `${diffDays}d`;
|
|
19102
|
+
}
|
|
19103
|
+
function clonePrEntry(pr) {
|
|
19104
|
+
return {
|
|
19105
|
+
...pr,
|
|
19106
|
+
ciChecks: pr.ciChecks.map((check) => ({ ...check })),
|
|
19107
|
+
comments: pr.comments.map((comment) => ({ ...comment }))
|
|
19108
|
+
};
|
|
19109
|
+
}
|
|
19110
|
+
function mapCreationSnapshot(creating) {
|
|
19111
|
+
return creating ? {
|
|
19112
|
+
phase: creating.phase
|
|
19113
|
+
} : null;
|
|
19114
|
+
}
|
|
19115
|
+
function mapWorktreeSnapshot(state, now, creating, isArchived, findLinearIssue, findAgentLabel) {
|
|
19116
|
+
return {
|
|
19117
|
+
branch: state.branch,
|
|
19118
|
+
label: state.label,
|
|
19119
|
+
...state.baseBranch ? { baseBranch: state.baseBranch } : {},
|
|
19120
|
+
path: state.path,
|
|
19121
|
+
dir: state.path,
|
|
19122
|
+
archived: isArchived(state.path),
|
|
19123
|
+
profile: state.profile,
|
|
19124
|
+
agentName: state.agentName,
|
|
19125
|
+
agentLabel: findAgentLabel ? findAgentLabel(state.agentName) : state.agentName,
|
|
19126
|
+
agentTerminalStale: state.agentTerminalStale,
|
|
19127
|
+
mux: state.session.exists,
|
|
19128
|
+
dirty: state.git.dirty,
|
|
19129
|
+
unpushed: state.git.aheadCount > 0,
|
|
19130
|
+
paneCount: state.session.paneCount,
|
|
19131
|
+
status: creating ? "creating" : state.agent.lifecycle,
|
|
19132
|
+
elapsed: formatElapsedSince(state.agent.lastStartedAt, now),
|
|
19133
|
+
services: state.services.map((service) => ({ ...service })),
|
|
19134
|
+
prs: state.prs.map((pr) => clonePrEntry(pr)),
|
|
19135
|
+
linearIssue: findLinearIssue ? findLinearIssue(state.branch) : null,
|
|
19136
|
+
creation: mapCreationSnapshot(creating),
|
|
19137
|
+
source: state.source,
|
|
19138
|
+
oneshot: state.oneshot
|
|
19139
|
+
};
|
|
19140
|
+
}
|
|
19141
|
+
function mapCreatingWorktreeSnapshot(creating, isArchived, findLinearIssue, findAgentLabel) {
|
|
19142
|
+
return {
|
|
19143
|
+
branch: creating.branch,
|
|
19144
|
+
label: null,
|
|
19145
|
+
...creating.baseBranch ? { baseBranch: creating.baseBranch } : {},
|
|
19146
|
+
path: creating.path,
|
|
19147
|
+
dir: creating.path,
|
|
19148
|
+
archived: isArchived(creating.path),
|
|
19149
|
+
profile: creating.profile,
|
|
19150
|
+
agentName: creating.agentName,
|
|
19151
|
+
agentLabel: findAgentLabel ? findAgentLabel(creating.agentName) : creating.agentName,
|
|
19152
|
+
agentTerminalStale: false,
|
|
19153
|
+
mux: false,
|
|
19154
|
+
dirty: false,
|
|
19155
|
+
unpushed: false,
|
|
19156
|
+
paneCount: 0,
|
|
19157
|
+
status: "creating",
|
|
19158
|
+
elapsed: "",
|
|
19159
|
+
services: [],
|
|
19160
|
+
prs: [],
|
|
19161
|
+
linearIssue: findLinearIssue ? findLinearIssue(creating.branch) : null,
|
|
19162
|
+
creation: mapCreationSnapshot(creating),
|
|
19163
|
+
source: creating.source,
|
|
19164
|
+
oneshot: null
|
|
19165
|
+
};
|
|
19166
|
+
}
|
|
19167
|
+
function buildWorktreeSnapshots(input) {
|
|
19168
|
+
const now = input.now ?? (() => new Date);
|
|
19169
|
+
const isArchived = input.isArchived ?? (() => false);
|
|
19170
|
+
const creatingWorktrees = input.creatingWorktrees ?? [];
|
|
19171
|
+
const creatingByBranch = new Map(creatingWorktrees.map((worktree) => [worktree.branch, worktree]));
|
|
19172
|
+
const runtimeWorktrees = input.runtime.listWorktrees();
|
|
19173
|
+
const runtimeBranches = new Set(runtimeWorktrees.map((worktree) => worktree.branch));
|
|
19174
|
+
const worktrees = runtimeWorktrees.map((state) => mapWorktreeSnapshot(state, now, creatingByBranch.get(state.branch) ?? null, isArchived, input.findLinearIssue, input.findAgentLabel));
|
|
19175
|
+
for (const creating of creatingWorktrees) {
|
|
19176
|
+
if (!runtimeBranches.has(creating.branch)) {
|
|
19177
|
+
worktrees.push(mapCreatingWorktreeSnapshot(creating, isArchived, input.findLinearIssue, input.findAgentLabel));
|
|
19178
|
+
}
|
|
19179
|
+
}
|
|
19180
|
+
worktrees.sort((left, right) => left.branch.localeCompare(right.branch));
|
|
19181
|
+
return worktrees;
|
|
19182
|
+
}
|
|
19183
|
+
function buildProjectSnapshot(input) {
|
|
19184
|
+
return {
|
|
19185
|
+
project: {
|
|
19186
|
+
name: input.projectName,
|
|
19187
|
+
mainBranch: input.mainBranch
|
|
19188
|
+
},
|
|
19189
|
+
worktrees: buildWorktreeSnapshots(input),
|
|
19190
|
+
notifications: input.notifications.map((notification) => ({ ...notification }))
|
|
19191
|
+
};
|
|
19192
|
+
}
|
|
19193
|
+
|
|
19194
|
+
// backend/src/services/claude-conversation-service.ts
|
|
19195
|
+
function isClaudeWorktree(worktree) {
|
|
19196
|
+
return worktree.agentName === "claude";
|
|
19197
|
+
}
|
|
19198
|
+
function isClaudeConversationMeta(meta) {
|
|
19199
|
+
return meta?.provider === "claudeCode";
|
|
19200
|
+
}
|
|
19201
|
+
function buildPendingConversationId(worktree) {
|
|
19202
|
+
return `claude-pending:${worktree.path}`;
|
|
19203
|
+
}
|
|
19204
|
+
function buildClaudeConversationMeta(sessionId, cwd, now) {
|
|
19205
|
+
return {
|
|
19206
|
+
provider: "claudeCode",
|
|
19207
|
+
conversationId: sessionId,
|
|
19208
|
+
sessionId,
|
|
19209
|
+
cwd,
|
|
19210
|
+
lastSeenAt: now.toISOString()
|
|
19211
|
+
};
|
|
19212
|
+
}
|
|
19213
|
+
function sameConversationMeta2(left, right) {
|
|
19214
|
+
return left?.provider === right.provider && left.conversationId === right.conversationId && left.cwd === right.cwd;
|
|
19215
|
+
}
|
|
19216
|
+
function normalizeSessionMessages(messages) {
|
|
19217
|
+
return messages.map((message, order) => ({
|
|
18012
19218
|
...message,
|
|
19219
|
+
order,
|
|
19220
|
+
kind: message.kind ?? "text",
|
|
18013
19221
|
status: "completed"
|
|
18014
19222
|
}));
|
|
18015
19223
|
}
|
|
18016
|
-
function
|
|
19224
|
+
function buildConversationState2(worktree, session) {
|
|
18017
19225
|
return {
|
|
18018
19226
|
provider: "claudeCode",
|
|
18019
19227
|
conversationId: session?.sessionId ?? buildPendingConversationId(worktree),
|
|
@@ -18023,10 +19231,10 @@ function buildConversationState(worktree, session) {
|
|
|
18023
19231
|
messages: normalizeSessionMessages(session?.messages ?? [])
|
|
18024
19232
|
};
|
|
18025
19233
|
}
|
|
18026
|
-
function
|
|
19234
|
+
function toWorktreeConversationResponse2(worktree, conversationMeta, session) {
|
|
18027
19235
|
return {
|
|
18028
19236
|
worktree: buildAgentsUiWorktreeSummary(worktree, conversationMeta),
|
|
18029
|
-
conversation:
|
|
19237
|
+
conversation: buildConversationState2(worktree, session)
|
|
18030
19238
|
};
|
|
18031
19239
|
}
|
|
18032
19240
|
|
|
@@ -18042,10 +19250,10 @@ class ClaudeConversationService {
|
|
|
18042
19250
|
this.writeMeta = deps.writeMeta ?? writeWorktreeMeta;
|
|
18043
19251
|
}
|
|
18044
19252
|
async attachWorktreeConversation(worktree) {
|
|
18045
|
-
return await this.withResolvedConversation(worktree, async (resolved) => ok(
|
|
19253
|
+
return await this.withResolvedConversation(worktree, async (resolved) => ok(toWorktreeConversationResponse2(worktree, resolved.conversationMeta, resolved.session)));
|
|
18046
19254
|
}
|
|
18047
19255
|
async readWorktreeConversation(worktree) {
|
|
18048
|
-
return await this.withResolvedConversation(worktree, async (resolved) => ok(
|
|
19256
|
+
return await this.withResolvedConversation(worktree, async (resolved) => ok(toWorktreeConversationResponse2(worktree, resolved.conversationMeta, resolved.session)));
|
|
18049
19257
|
}
|
|
18050
19258
|
async withResolvedConversation(worktree, fn) {
|
|
18051
19259
|
if (!isClaudeWorktree(worktree)) {
|
|
@@ -18094,7 +19302,7 @@ class ClaudeConversationService {
|
|
|
18094
19302
|
}
|
|
18095
19303
|
async persistConversationMeta(gitDir, meta, cwd, sessionId) {
|
|
18096
19304
|
const nextConversation = buildClaudeConversationMeta(sessionId, cwd, this.now());
|
|
18097
|
-
if (!
|
|
19305
|
+
if (!sameConversationMeta2(meta.conversation, nextConversation)) {
|
|
18098
19306
|
await this.writeMeta(gitDir, {
|
|
18099
19307
|
...meta,
|
|
18100
19308
|
conversation: nextConversation
|
|
@@ -18104,270 +19312,6 @@ class ClaudeConversationService {
|
|
|
18104
19312
|
}
|
|
18105
19313
|
}
|
|
18106
19314
|
|
|
18107
|
-
// backend/src/services/worktree-conversation-service.ts
|
|
18108
|
-
function resolveCodexAppServerLaunchContext(input) {
|
|
18109
|
-
if (input.worktree.agentName !== "codex" || input.meta.agent !== "codex") {
|
|
18110
|
-
return err(409, "Codex web chat is only available for Codex worktrees");
|
|
18111
|
-
}
|
|
18112
|
-
if (!input.profile) {
|
|
18113
|
-
return err(409, `Profile is missing for Codex web chat: ${input.meta.profile}`);
|
|
18114
|
-
}
|
|
18115
|
-
if (input.meta.runtime !== "host" || input.profile.runtime !== "host") {
|
|
18116
|
-
return err(409, "Codex web chat is only available for host-runtime worktrees. Use the terminal for Docker worktrees.");
|
|
18117
|
-
}
|
|
18118
|
-
if (input.profile.yolo !== true) {
|
|
18119
|
-
return err(409, "Codex web chat requires a yolo profile to preserve the Codex approval policy. Use the terminal for this worktree.");
|
|
18120
|
-
}
|
|
18121
|
-
return ok({
|
|
18122
|
-
approvalPolicy: "never",
|
|
18123
|
-
personality: "pragmatic",
|
|
18124
|
-
sandbox: "danger-full-access"
|
|
18125
|
-
});
|
|
18126
|
-
}
|
|
18127
|
-
function isCodexWorktree(worktree) {
|
|
18128
|
-
return worktree.agentName === "codex";
|
|
18129
|
-
}
|
|
18130
|
-
function isCodexConversationMeta(meta) {
|
|
18131
|
-
return meta?.provider === "codexAppServer";
|
|
18132
|
-
}
|
|
18133
|
-
function toIsoTimestamp(epochSeconds) {
|
|
18134
|
-
if (epochSeconds === null)
|
|
18135
|
-
return null;
|
|
18136
|
-
return new Date(epochSeconds * 1000).toISOString();
|
|
18137
|
-
}
|
|
18138
|
-
function isUserMessageItem(item) {
|
|
18139
|
-
return item.type === "userMessage";
|
|
18140
|
-
}
|
|
18141
|
-
function isAgentMessageItem(item) {
|
|
18142
|
-
return item.type === "agentMessage";
|
|
18143
|
-
}
|
|
18144
|
-
function extractUserText(item) {
|
|
18145
|
-
return item.content.map((contentItem) => contentItem.text ?? "").join("").trim();
|
|
18146
|
-
}
|
|
18147
|
-
function extractAgentText(item) {
|
|
18148
|
-
return item.text ?? item.message ?? "";
|
|
18149
|
-
}
|
|
18150
|
-
function isActiveTurnStatus(status) {
|
|
18151
|
-
return status === "inProgress" || status === "active" || status === "running" || status === "pending" || status === "queued";
|
|
18152
|
-
}
|
|
18153
|
-
function findActiveTurn(thread) {
|
|
18154
|
-
for (let index = thread.turns.length - 1;index >= 0; index -= 1) {
|
|
18155
|
-
const turn = thread.turns[index];
|
|
18156
|
-
if (isActiveTurnStatus(turn.status))
|
|
18157
|
-
return turn;
|
|
18158
|
-
}
|
|
18159
|
-
return null;
|
|
18160
|
-
}
|
|
18161
|
-
function buildConversationMessages(thread) {
|
|
18162
|
-
const messages = [];
|
|
18163
|
-
for (const turn of thread.turns) {
|
|
18164
|
-
for (const item of turn.items) {
|
|
18165
|
-
if (isUserMessageItem(item)) {
|
|
18166
|
-
const text2 = extractUserText(item);
|
|
18167
|
-
if (text2.length === 0)
|
|
18168
|
-
continue;
|
|
18169
|
-
messages.push({
|
|
18170
|
-
id: item.id,
|
|
18171
|
-
turnId: turn.id,
|
|
18172
|
-
role: "user",
|
|
18173
|
-
text: text2,
|
|
18174
|
-
status: "completed",
|
|
18175
|
-
createdAt: toIsoTimestamp(turn.startedAt)
|
|
18176
|
-
});
|
|
18177
|
-
continue;
|
|
18178
|
-
}
|
|
18179
|
-
if (!isAgentMessageItem(item))
|
|
18180
|
-
continue;
|
|
18181
|
-
const text = extractAgentText(item);
|
|
18182
|
-
if (text.length === 0)
|
|
18183
|
-
continue;
|
|
18184
|
-
messages.push({
|
|
18185
|
-
id: item.id,
|
|
18186
|
-
turnId: turn.id,
|
|
18187
|
-
role: "assistant",
|
|
18188
|
-
text,
|
|
18189
|
-
status: isActiveTurnStatus(turn.status) ? "inProgress" : "completed",
|
|
18190
|
-
createdAt: toIsoTimestamp(turn.completedAt ?? turn.startedAt)
|
|
18191
|
-
});
|
|
18192
|
-
}
|
|
18193
|
-
}
|
|
18194
|
-
return messages;
|
|
18195
|
-
}
|
|
18196
|
-
function buildConversationState2(thread) {
|
|
18197
|
-
const activeTurn = findActiveTurn(thread);
|
|
18198
|
-
return {
|
|
18199
|
-
provider: "codexAppServer",
|
|
18200
|
-
conversationId: thread.id,
|
|
18201
|
-
cwd: thread.cwd,
|
|
18202
|
-
running: thread.status.type === "active" || activeTurn !== null,
|
|
18203
|
-
activeTurnId: activeTurn?.id ?? null,
|
|
18204
|
-
messages: buildConversationMessages(thread)
|
|
18205
|
-
};
|
|
18206
|
-
}
|
|
18207
|
-
function selectDiscoveredThread(threads) {
|
|
18208
|
-
if (threads.length === 0)
|
|
18209
|
-
return null;
|
|
18210
|
-
return [...threads].sort((left, right) => right.updatedAt - left.updatedAt)[0] ?? null;
|
|
18211
|
-
}
|
|
18212
|
-
function buildConversationMeta(thread, now) {
|
|
18213
|
-
return {
|
|
18214
|
-
provider: "codexAppServer",
|
|
18215
|
-
conversationId: thread.id,
|
|
18216
|
-
threadId: thread.id,
|
|
18217
|
-
cwd: thread.cwd,
|
|
18218
|
-
lastSeenAt: now.toISOString()
|
|
18219
|
-
};
|
|
18220
|
-
}
|
|
18221
|
-
function sameConversationMeta2(left, right) {
|
|
18222
|
-
return left?.provider === right.provider && left.conversationId === right.conversationId && left.cwd === right.cwd;
|
|
18223
|
-
}
|
|
18224
|
-
function toWorktreeConversationResponse2(worktree, conversationMeta, thread) {
|
|
18225
|
-
return {
|
|
18226
|
-
worktree: buildAgentsUiWorktreeSummary(worktree, conversationMeta),
|
|
18227
|
-
conversation: buildConversationState2(thread)
|
|
18228
|
-
};
|
|
18229
|
-
}
|
|
18230
|
-
|
|
18231
|
-
class WorktreeConversationService {
|
|
18232
|
-
deps;
|
|
18233
|
-
now;
|
|
18234
|
-
readMeta;
|
|
18235
|
-
writeMeta;
|
|
18236
|
-
constructor(deps) {
|
|
18237
|
-
this.deps = deps;
|
|
18238
|
-
this.now = deps.now ?? (() => new Date);
|
|
18239
|
-
this.readMeta = deps.readMeta ?? readWorktreeMeta;
|
|
18240
|
-
this.writeMeta = deps.writeMeta ?? writeWorktreeMeta;
|
|
18241
|
-
}
|
|
18242
|
-
async attachWorktreeConversation(worktree) {
|
|
18243
|
-
return await this.withResolvedConversation(worktree, true, async ({ conversationMeta, thread }) => ok(toWorktreeConversationResponse2(worktree, conversationMeta, thread)));
|
|
18244
|
-
}
|
|
18245
|
-
async readWorktreeConversation(worktree) {
|
|
18246
|
-
return await this.withResolvedConversation(worktree, false, async ({ conversationMeta, thread }) => ok(toWorktreeConversationResponse2(worktree, conversationMeta, thread)));
|
|
18247
|
-
}
|
|
18248
|
-
async sendWorktreeConversationMessage(worktree, text) {
|
|
18249
|
-
return await this.withResolvedConversation(worktree, true, async ({ thread, launchContext }) => {
|
|
18250
|
-
const started = await this.deps.appServer.turnStart({
|
|
18251
|
-
threadId: thread.id,
|
|
18252
|
-
cwd: worktree.path,
|
|
18253
|
-
approvalPolicy: launchContext.approvalPolicy,
|
|
18254
|
-
input: [{ type: "text", text }]
|
|
18255
|
-
});
|
|
18256
|
-
return ok({
|
|
18257
|
-
conversationId: thread.id,
|
|
18258
|
-
turnId: started.turn.id,
|
|
18259
|
-
running: true
|
|
18260
|
-
});
|
|
18261
|
-
});
|
|
18262
|
-
}
|
|
18263
|
-
async interruptWorktreeConversation(worktree) {
|
|
18264
|
-
return await this.withResolvedConversation(worktree, false, async ({ thread }) => {
|
|
18265
|
-
const conversation = buildConversationState2(thread);
|
|
18266
|
-
const turnId = conversation.activeTurnId;
|
|
18267
|
-
if (!turnId) {
|
|
18268
|
-
return err(409, "No active Codex turn to interrupt");
|
|
18269
|
-
}
|
|
18270
|
-
await this.deps.appServer.turnInterrupt({
|
|
18271
|
-
threadId: thread.id,
|
|
18272
|
-
turnId
|
|
18273
|
-
});
|
|
18274
|
-
return ok({
|
|
18275
|
-
conversationId: thread.id,
|
|
18276
|
-
turnId,
|
|
18277
|
-
interrupted: true
|
|
18278
|
-
});
|
|
18279
|
-
});
|
|
18280
|
-
}
|
|
18281
|
-
async withResolvedConversation(worktree, allowCreate, fn) {
|
|
18282
|
-
if (!isCodexWorktree(worktree)) {
|
|
18283
|
-
return err(409, "Worktree chat is only available for Codex worktrees");
|
|
18284
|
-
}
|
|
18285
|
-
try {
|
|
18286
|
-
const resolved = await this.resolveConversation(worktree, allowCreate);
|
|
18287
|
-
if (!resolved.ok)
|
|
18288
|
-
return resolved;
|
|
18289
|
-
return await fn(resolved.data);
|
|
18290
|
-
} catch (error) {
|
|
18291
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
18292
|
-
return err(502, message);
|
|
18293
|
-
}
|
|
18294
|
-
}
|
|
18295
|
-
async resolveConversation(worktree, allowCreate) {
|
|
18296
|
-
const gitDir = this.deps.git.resolveWorktreeGitDir(worktree.path);
|
|
18297
|
-
const meta = await this.readMeta(gitDir);
|
|
18298
|
-
if (!meta) {
|
|
18299
|
-
return err(409, "Worktree metadata is missing");
|
|
18300
|
-
}
|
|
18301
|
-
const launchContextResult = await this.deps.resolveLaunchContext({ worktree, meta });
|
|
18302
|
-
if (!launchContextResult.ok)
|
|
18303
|
-
return launchContextResult;
|
|
18304
|
-
const launchContext = launchContextResult.data;
|
|
18305
|
-
const now = this.now();
|
|
18306
|
-
const thread = await this.resolveThread(meta, worktree.path, allowCreate, launchContext);
|
|
18307
|
-
if (!thread) {
|
|
18308
|
-
return err(404, "No Codex thread could be resolved for this worktree");
|
|
18309
|
-
}
|
|
18310
|
-
const conversationMeta = buildConversationMeta(thread, now);
|
|
18311
|
-
const nextMeta = sameConversationMeta2(meta.conversation, conversationMeta) ? { ...meta, conversation: { ...conversationMeta, lastSeenAt: meta.conversation?.lastSeenAt ?? conversationMeta.lastSeenAt } } : { ...meta, conversation: conversationMeta };
|
|
18312
|
-
if (!sameConversationMeta2(meta.conversation, conversationMeta)) {
|
|
18313
|
-
await this.writeMeta(gitDir, nextMeta);
|
|
18314
|
-
}
|
|
18315
|
-
return ok({
|
|
18316
|
-
gitDir,
|
|
18317
|
-
meta: nextMeta,
|
|
18318
|
-
thread,
|
|
18319
|
-
conversationMeta: nextMeta.conversation ?? conversationMeta,
|
|
18320
|
-
launchContext
|
|
18321
|
-
});
|
|
18322
|
-
}
|
|
18323
|
-
async resolveThread(meta, cwd, allowCreate, launchContext) {
|
|
18324
|
-
const discoveredThread = selectDiscoveredThread((await this.deps.appServer.threadList({
|
|
18325
|
-
cwd,
|
|
18326
|
-
limit: 20,
|
|
18327
|
-
sortKey: "updated_at"
|
|
18328
|
-
})).data);
|
|
18329
|
-
if (discoveredThread) {
|
|
18330
|
-
return await this.ensureThreadLoaded(discoveredThread.id, cwd, launchContext);
|
|
18331
|
-
}
|
|
18332
|
-
const savedThreadId = isCodexConversationMeta(meta.conversation) ? meta.conversation.threadId : null;
|
|
18333
|
-
if (savedThreadId) {
|
|
18334
|
-
const savedThread = await this.tryLoadThread(savedThreadId, cwd, launchContext);
|
|
18335
|
-
if (savedThread)
|
|
18336
|
-
return savedThread;
|
|
18337
|
-
log.warn(`[agents] saved codex thread missing, rediscovering cwd=${cwd} threadId=${savedThreadId}`);
|
|
18338
|
-
}
|
|
18339
|
-
if (!allowCreate)
|
|
18340
|
-
return null;
|
|
18341
|
-
const started = await this.deps.appServer.threadStart({
|
|
18342
|
-
cwd,
|
|
18343
|
-
approvalPolicy: launchContext.approvalPolicy,
|
|
18344
|
-
personality: launchContext.personality,
|
|
18345
|
-
sandbox: launchContext.sandbox
|
|
18346
|
-
});
|
|
18347
|
-
return started.thread;
|
|
18348
|
-
}
|
|
18349
|
-
async tryLoadThread(threadId, cwd, launchContext) {
|
|
18350
|
-
try {
|
|
18351
|
-
return await this.ensureThreadLoaded(threadId, cwd, launchContext);
|
|
18352
|
-
} catch {
|
|
18353
|
-
return null;
|
|
18354
|
-
}
|
|
18355
|
-
}
|
|
18356
|
-
async ensureThreadLoaded(threadId, cwd, launchContext) {
|
|
18357
|
-
const initial = await this.deps.appServer.threadRead(threadId, false);
|
|
18358
|
-
if (initial.thread.status.type === "notLoaded") {
|
|
18359
|
-
await this.deps.appServer.threadResume({
|
|
18360
|
-
threadId,
|
|
18361
|
-
cwd,
|
|
18362
|
-
approvalPolicy: launchContext.approvalPolicy,
|
|
18363
|
-
personality: launchContext.personality,
|
|
18364
|
-
sandbox: launchContext.sandbox
|
|
18365
|
-
});
|
|
18366
|
-
}
|
|
18367
|
-
return (await this.deps.appServer.threadRead(threadId, true)).thread;
|
|
18368
|
-
}
|
|
18369
|
-
}
|
|
18370
|
-
|
|
18371
19315
|
// backend/src/domain/events.ts
|
|
18372
19316
|
function hasBaseFields(raw) {
|
|
18373
19317
|
return typeof raw.worktreeId === "string" && raw.worktreeId.length > 0 && typeof raw.branch === "string" && raw.branch.length > 0 && typeof raw.type === "string" && ["agent_stopped", "agent_status_changed", "pr_opened", "runtime_error"].includes(raw.type);
|
|
@@ -20190,7 +21134,7 @@ async function apiRefreshWorktreeAgentTerminal(branch) {
|
|
|
20190
21134
|
await lifecycleService.refreshAgentTerminal(branch);
|
|
20191
21135
|
return jsonResponse({ ok: true });
|
|
20192
21136
|
}
|
|
20193
|
-
async function
|
|
21137
|
+
async function loadAgentsConversationInitialState(branch) {
|
|
20194
21138
|
const resolved = await resolveAgentsWorktree(branch);
|
|
20195
21139
|
if (!resolved.ok) {
|
|
20196
21140
|
return {
|
|
@@ -20221,45 +21165,55 @@ async function readErrorMessage(response) {
|
|
|
20221
21165
|
const text = await response.text();
|
|
20222
21166
|
return text.length > 0 ? text : `HTTP ${response.status}`;
|
|
20223
21167
|
}
|
|
21168
|
+
function nextConversationMessageOrder(messages) {
|
|
21169
|
+
return messages.reduce((order, message) => Math.max(order, message.order + 1), 0);
|
|
21170
|
+
}
|
|
20224
21171
|
async function openAgentsSocket(ws, data) {
|
|
20225
|
-
|
|
20226
|
-
|
|
20227
|
-
|
|
20228
|
-
|
|
21172
|
+
let bufferingNotifications = true;
|
|
21173
|
+
let socketClosed = false;
|
|
21174
|
+
let streamSession = null;
|
|
21175
|
+
const bufferedNotifications = [];
|
|
21176
|
+
const unsubscribeNotifications = codexAppServerClient.onNotification((notification) => {
|
|
21177
|
+
if (bufferingNotifications || !streamSession) {
|
|
21178
|
+
bufferedNotifications.push(notification);
|
|
21179
|
+
return;
|
|
21180
|
+
}
|
|
21181
|
+
const notificationThreadId = readAgentsNotificationThreadId(notification);
|
|
21182
|
+
if (!notificationThreadId || notificationThreadId !== data.conversationId)
|
|
21183
|
+
return;
|
|
21184
|
+
streamSession.handleNotification(notification);
|
|
21185
|
+
data.conversationId = streamSession.currentConversationId();
|
|
21186
|
+
});
|
|
21187
|
+
data.unsubscribe = () => {
|
|
21188
|
+
socketClosed = true;
|
|
21189
|
+
streamSession?.close();
|
|
21190
|
+
unsubscribeNotifications();
|
|
21191
|
+
};
|
|
21192
|
+
const initialState = await loadAgentsConversationInitialState(data.branch);
|
|
21193
|
+
if (socketClosed)
|
|
21194
|
+
return;
|
|
21195
|
+
if (!initialState.ok) {
|
|
21196
|
+
unsubscribeNotifications();
|
|
21197
|
+
sendAgentsWs(ws, { type: "error", message: initialState.message });
|
|
21198
|
+
ws.close(1011, initialState.message.slice(0, 123));
|
|
20229
21199
|
return;
|
|
20230
21200
|
}
|
|
20231
|
-
|
|
20232
|
-
|
|
20233
|
-
|
|
20234
|
-
|
|
21201
|
+
streamSession = new AgentsConversationStreamSession({
|
|
21202
|
+
conversationId: initialState.data.conversation.conversationId,
|
|
21203
|
+
nextOrder: nextConversationMessageOrder(initialState.data.conversation.messages),
|
|
21204
|
+
send: (event) => sendAgentsWs(ws, event)
|
|
20235
21205
|
});
|
|
20236
|
-
|
|
21206
|
+
data.conversationId = streamSession.currentConversationId();
|
|
21207
|
+
if (initialState.data.conversation.provider !== "codexAppServer") {
|
|
21208
|
+
unsubscribeNotifications();
|
|
21209
|
+
data.unsubscribe = null;
|
|
20237
21210
|
return;
|
|
20238
21211
|
}
|
|
20239
|
-
|
|
20240
|
-
|
|
20241
|
-
|
|
20242
|
-
|
|
20243
|
-
|
|
20244
|
-
if (deltaEvent) {
|
|
20245
|
-
sendAgentsWs(ws, deltaEvent);
|
|
20246
|
-
return;
|
|
20247
|
-
}
|
|
20248
|
-
if (!shouldRefreshAgentsConversationSnapshot(notification))
|
|
20249
|
-
return;
|
|
20250
|
-
(async () => {
|
|
20251
|
-
const nextSnapshot = await loadAgentsConversationSnapshot(data.branch);
|
|
20252
|
-
if (!nextSnapshot.ok) {
|
|
20253
|
-
sendAgentsWs(ws, { type: "error", message: nextSnapshot.message });
|
|
20254
|
-
return;
|
|
20255
|
-
}
|
|
20256
|
-
data.conversationId = nextSnapshot.data.conversation.conversationId;
|
|
20257
|
-
sendAgentsWs(ws, {
|
|
20258
|
-
type: "snapshot",
|
|
20259
|
-
data: nextSnapshot.data
|
|
20260
|
-
});
|
|
20261
|
-
})();
|
|
20262
|
-
});
|
|
21212
|
+
bufferingNotifications = false;
|
|
21213
|
+
for (const notification of bufferedNotifications) {
|
|
21214
|
+
streamSession.handleNotification(notification);
|
|
21215
|
+
data.conversationId = streamSession.currentConversationId();
|
|
21216
|
+
}
|
|
20263
21217
|
}
|
|
20264
21218
|
async function apiRuntimeEvent(req) {
|
|
20265
21219
|
if (!await hasValidControlToken(req)) {
|