@getpaseo/server 0.1.86 → 0.1.88
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/dist/server/server/agent/mcp-server.js +34 -3
- package/dist/server/server/agent/mcp-shared.d.ts +1 -0
- package/dist/server/server/agent/providers/opencode-agent.js +4 -1
- package/dist/server/server/agent/timeline-projection.d.ts +17 -1
- package/dist/server/server/agent/timeline-projection.js +82 -17
- package/dist/server/server/persisted-config.d.ts +5 -5
- package/dist/server/server/schedule/cron.js +52 -5
- package/dist/server/server/session.d.ts +4 -1
- package/dist/server/server/session.js +74 -89
- package/dist/server/terminal/terminal-manager.js +11 -1
- package/dist/server/terminal/terminal-session-controller.d.ts +3 -1
- package/dist/server/terminal/terminal-session-controller.js +22 -12
- package/dist/server/terminal/terminal.d.ts +1 -0
- package/dist/server/terminal/terminal.js +34 -0
- package/dist/server/utils/checkout-git.js +119 -47
- package/package.json +5 -5
|
@@ -176,17 +176,28 @@ function normalizeScheduleCadenceArg(value) {
|
|
|
176
176
|
}
|
|
177
177
|
return trimmed;
|
|
178
178
|
}
|
|
179
|
+
function normalizeScheduleTimeZoneArg(value) {
|
|
180
|
+
return normalizeScheduleCadenceArg(value);
|
|
181
|
+
}
|
|
179
182
|
function resolveScheduleUpdateCadence(input) {
|
|
180
183
|
const every = normalizeScheduleCadenceArg(input.every);
|
|
181
184
|
const cron = normalizeScheduleCadenceArg(input.cron);
|
|
185
|
+
const timeZone = normalizeScheduleTimeZoneArg(input.timezone);
|
|
182
186
|
if (every !== undefined && cron !== undefined) {
|
|
183
187
|
throw new Error("Specify at most one of every or cron");
|
|
184
188
|
}
|
|
189
|
+
if (timeZone !== undefined && cron === undefined) {
|
|
190
|
+
throw new Error("timezone can only be used with cron");
|
|
191
|
+
}
|
|
185
192
|
if (every !== undefined) {
|
|
186
193
|
return { type: "every", everyMs: parseDurationString(every) };
|
|
187
194
|
}
|
|
188
195
|
if (cron !== undefined) {
|
|
189
|
-
return {
|
|
196
|
+
return {
|
|
197
|
+
type: "cron",
|
|
198
|
+
expression: cron,
|
|
199
|
+
...(timeZone !== undefined ? { timezone: timeZone } : {}),
|
|
200
|
+
};
|
|
190
201
|
}
|
|
191
202
|
return undefined;
|
|
192
203
|
}
|
|
@@ -1162,6 +1173,12 @@ export async function createAgentMcpServer(options) {
|
|
|
1162
1173
|
prompt: z.string().trim().min(1, "prompt is required"),
|
|
1163
1174
|
every: z.string().optional(),
|
|
1164
1175
|
cron: z.string().optional(),
|
|
1176
|
+
timezone: z
|
|
1177
|
+
.string()
|
|
1178
|
+
.trim()
|
|
1179
|
+
.min(1)
|
|
1180
|
+
.optional()
|
|
1181
|
+
.describe("IANA time zone for cron cadence; requires cron. For example: America/New_York."),
|
|
1165
1182
|
name: z.string().optional(),
|
|
1166
1183
|
target: z.enum(["self", "new-agent"]).optional(),
|
|
1167
1184
|
provider: AgentProviderEnum.optional().describe("Provider, or provider/model (for example: codex or codex/gpt-5.4)."),
|
|
@@ -1170,16 +1187,20 @@ export async function createAgentMcpServer(options) {
|
|
|
1170
1187
|
expiresIn: z.string().optional(),
|
|
1171
1188
|
},
|
|
1172
1189
|
outputSchema: ScheduleSummarySchema.shape,
|
|
1173
|
-
}, async ({ prompt, every, cron, name, target, provider, cwd, maxRuns, expiresIn }) => {
|
|
1190
|
+
}, async ({ prompt, every, cron, timezone, name, target, provider, cwd, maxRuns, expiresIn }) => {
|
|
1174
1191
|
if (!scheduleService) {
|
|
1175
1192
|
throw new Error("Schedule service is not configured");
|
|
1176
1193
|
}
|
|
1177
1194
|
const normalizedEvery = normalizeScheduleCadenceArg(every);
|
|
1178
1195
|
const normalizedCron = normalizeScheduleCadenceArg(cron);
|
|
1196
|
+
const normalizedTimeZone = normalizeScheduleTimeZoneArg(timezone);
|
|
1179
1197
|
const cadenceCount = Number(normalizedEvery !== undefined) + Number(normalizedCron !== undefined);
|
|
1180
1198
|
if (cadenceCount !== 1) {
|
|
1181
1199
|
throw new Error("Specify exactly one of every or cron");
|
|
1182
1200
|
}
|
|
1201
|
+
if (normalizedTimeZone !== undefined && normalizedCron === undefined) {
|
|
1202
|
+
throw new Error("timezone can only be used with cron");
|
|
1203
|
+
}
|
|
1183
1204
|
const scheduleTarget = target === "self"
|
|
1184
1205
|
? (() => {
|
|
1185
1206
|
const callerAgent = resolveCallerAgent();
|
|
@@ -1209,7 +1230,11 @@ export async function createAgentMcpServer(options) {
|
|
|
1209
1230
|
prompt: prompt.trim(),
|
|
1210
1231
|
cadence: normalizedEvery !== undefined
|
|
1211
1232
|
? { type: "every", everyMs: parseDurationString(normalizedEvery) }
|
|
1212
|
-
: {
|
|
1233
|
+
: {
|
|
1234
|
+
type: "cron",
|
|
1235
|
+
expression: normalizedCron,
|
|
1236
|
+
...(normalizedTimeZone !== undefined ? { timezone: normalizedTimeZone } : {}),
|
|
1237
|
+
},
|
|
1213
1238
|
target: scheduleTarget,
|
|
1214
1239
|
...(name?.trim() ? { name: name.trim() } : {}),
|
|
1215
1240
|
...(maxRuns === undefined ? {} : { maxRuns }),
|
|
@@ -1320,6 +1345,12 @@ export async function createAgentMcpServer(options) {
|
|
|
1320
1345
|
id: z.string(),
|
|
1321
1346
|
every: z.string().optional().describe("New interval duration string (e.g. 5m, 1h)."),
|
|
1322
1347
|
cron: z.string().optional().describe("New cron expression."),
|
|
1348
|
+
timezone: z
|
|
1349
|
+
.string()
|
|
1350
|
+
.trim()
|
|
1351
|
+
.min(1)
|
|
1352
|
+
.optional()
|
|
1353
|
+
.describe("IANA time zone for cron cadence; requires cron. For example: America/New_York."),
|
|
1323
1354
|
name: z.string().nullable().optional().describe("New name (null to clear)."),
|
|
1324
1355
|
prompt: z.string().trim().min(1).optional().describe("New prompt text."),
|
|
1325
1356
|
maxRuns: z
|
|
@@ -1371,6 +1371,9 @@ function getOpenCodeSubAgentMaps(state) {
|
|
|
1371
1371
|
pendingChildToolPartsBySessionId: state.pendingChildToolPartsBySessionId,
|
|
1372
1372
|
};
|
|
1373
1373
|
}
|
|
1374
|
+
function isOpenCodeSessionTrackedByParent(sessionId, state) {
|
|
1375
|
+
return (sessionId === state.sessionId || state.subAgentCallIdByChildSessionId?.has(sessionId) === true);
|
|
1376
|
+
}
|
|
1374
1377
|
function getOpenCodeSubAgentState(callId, state, toolCall) {
|
|
1375
1378
|
const maps = getOpenCodeSubAgentMaps(state);
|
|
1376
1379
|
const existing = maps.byCallId.get(callId);
|
|
@@ -1738,7 +1741,7 @@ function appendOpenCodeMessagePartDelta(event, state, events) {
|
|
|
1738
1741
|
});
|
|
1739
1742
|
}
|
|
1740
1743
|
function appendOpenCodePermissionAsked(event, state, events) {
|
|
1741
|
-
if (event.properties.sessionID
|
|
1744
|
+
if (!isOpenCodeSessionTrackedByParent(event.properties.sessionID, state)) {
|
|
1742
1745
|
return;
|
|
1743
1746
|
}
|
|
1744
1747
|
const metadata = readOpenCodeRecord(event.properties.metadata);
|
|
@@ -21,6 +21,13 @@ interface ProjectedWindowSelection {
|
|
|
21
21
|
minSeq: number | null;
|
|
22
22
|
maxSeq: number | null;
|
|
23
23
|
}
|
|
24
|
+
export interface ProjectedTimelinePageSelection {
|
|
25
|
+
entries: TimelineProjectionEntry[];
|
|
26
|
+
startSeq: number | null;
|
|
27
|
+
endSeq: number | null;
|
|
28
|
+
hasOlder: boolean;
|
|
29
|
+
hasNewer: boolean;
|
|
30
|
+
}
|
|
24
31
|
export declare function projectTimelineRows(input: {
|
|
25
32
|
rows: readonly AgentTimelineRow[];
|
|
26
33
|
mode: TimelineProjectionMode;
|
|
@@ -34,8 +41,17 @@ export declare function selectTimelineWindowByProjectedLimit(input: {
|
|
|
34
41
|
rows: readonly AgentTimelineRow[];
|
|
35
42
|
direction: TimelineLimitDirection;
|
|
36
43
|
limit: number;
|
|
37
|
-
collapseToolLifecycle?: boolean;
|
|
38
44
|
}): ProjectedWindowSelection;
|
|
45
|
+
export declare function selectProjectedTimelinePage(input: {
|
|
46
|
+
rows: readonly AgentTimelineRow[];
|
|
47
|
+
bounds?: {
|
|
48
|
+
minSeq: number;
|
|
49
|
+
maxSeq: number;
|
|
50
|
+
};
|
|
51
|
+
direction: TimelineLimitDirection;
|
|
52
|
+
cursorSeq?: number;
|
|
53
|
+
limit?: number;
|
|
54
|
+
}): ProjectedTimelinePageSelection;
|
|
39
55
|
/**
|
|
40
56
|
* Apply a projected-count limit to a flat AgentTimelineItem[] without seq metadata.
|
|
41
57
|
* Used by callers that only have items in hand (e.g. MCP tools reading
|
|
@@ -188,9 +188,8 @@ export function projectTimelineRows(input) {
|
|
|
188
188
|
export function selectTimelineWindowByProjectedLimit(input) {
|
|
189
189
|
const { rows, direction } = input;
|
|
190
190
|
const limit = Math.max(0, Math.floor(input.limit));
|
|
191
|
-
const collapseTools = input.collapseToolLifecycle ?? true;
|
|
192
191
|
const canonical = makeCanonicalEntries(rows);
|
|
193
|
-
const projectedAll = mergeReasoningChunks(mergeAssistantChunks(
|
|
192
|
+
const projectedAll = mergeReasoningChunks(mergeAssistantChunks(collapseToolLifecycle(canonical)));
|
|
194
193
|
if (projectedAll.length === 0) {
|
|
195
194
|
return {
|
|
196
195
|
projectedEntries: [],
|
|
@@ -232,23 +231,21 @@ export function selectTimelineWindowByProjectedLimit(input) {
|
|
|
232
231
|
};
|
|
233
232
|
let { minSeq, maxSeq } = computeWindowBounds(projectedEntries);
|
|
234
233
|
let expandedEntries = projectedEntries;
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
nextBounds.maxSeq === maxSeq) {
|
|
245
|
-
expandedEntries = overlapping;
|
|
246
|
-
break;
|
|
247
|
-
}
|
|
234
|
+
// Expand to include any projected entries that overlap the selected canonical
|
|
235
|
+
// range. Tool lifecycle collapse can produce non-monotonic seqEnd values,
|
|
236
|
+
// which would otherwise create cursor gaps.
|
|
237
|
+
for (let iteration = 0; iteration < projectedAll.length + 1; iteration += 1) {
|
|
238
|
+
const overlapping = projectedAll.filter((entry) => entry.seqStart <= maxSeq && entry.seqEnd >= minSeq);
|
|
239
|
+
const nextBounds = computeWindowBounds(overlapping);
|
|
240
|
+
if (overlapping.length === expandedEntries.length &&
|
|
241
|
+
nextBounds.minSeq === minSeq &&
|
|
242
|
+
nextBounds.maxSeq === maxSeq) {
|
|
248
243
|
expandedEntries = overlapping;
|
|
249
|
-
|
|
250
|
-
maxSeq = nextBounds.maxSeq;
|
|
244
|
+
break;
|
|
251
245
|
}
|
|
246
|
+
expandedEntries = overlapping;
|
|
247
|
+
minSeq = nextBounds.minSeq;
|
|
248
|
+
maxSeq = nextBounds.maxSeq;
|
|
252
249
|
}
|
|
253
250
|
const selectedRows = rows.filter((row) => row.seq >= minSeq && row.seq <= maxSeq);
|
|
254
251
|
return {
|
|
@@ -258,6 +255,74 @@ export function selectTimelineWindowByProjectedLimit(input) {
|
|
|
258
255
|
maxSeq: Number.isFinite(maxSeq) ? maxSeq : null,
|
|
259
256
|
};
|
|
260
257
|
}
|
|
258
|
+
function getTimelineBounds(rows) {
|
|
259
|
+
const first = rows[0];
|
|
260
|
+
const last = rows[rows.length - 1];
|
|
261
|
+
if (!first || !last) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return { minSeq: first.seq, maxSeq: last.seq };
|
|
265
|
+
}
|
|
266
|
+
function selectEntriesOverlappingSeqRange(input) {
|
|
267
|
+
return input.entries.filter((entry) => entry.seqStart <= input.endSeq && entry.seqEnd >= input.startSeq);
|
|
268
|
+
}
|
|
269
|
+
export function selectProjectedTimelinePage(input) {
|
|
270
|
+
const limit = input.limit === undefined ? 0 : Math.max(0, Math.floor(input.limit));
|
|
271
|
+
const bounds = input.bounds ?? getTimelineBounds(input.rows);
|
|
272
|
+
const projectedAll = projectTimelineRows({ rows: input.rows, mode: "projected" });
|
|
273
|
+
if (projectedAll.length === 0 || !bounds) {
|
|
274
|
+
return {
|
|
275
|
+
entries: [],
|
|
276
|
+
startSeq: null,
|
|
277
|
+
endSeq: null,
|
|
278
|
+
hasOlder: false,
|
|
279
|
+
hasNewer: false,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (input.direction === "tail") {
|
|
283
|
+
const selected = selectTimelineWindowByProjectedLimit({
|
|
284
|
+
rows: input.rows,
|
|
285
|
+
direction: "tail",
|
|
286
|
+
limit,
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
entries: selected.projectedEntries,
|
|
290
|
+
startSeq: selected.minSeq,
|
|
291
|
+
endSeq: selected.maxSeq,
|
|
292
|
+
hasOlder: selected.minSeq !== null && selected.minSeq > bounds.minSeq,
|
|
293
|
+
hasNewer: false,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
let startSeq;
|
|
297
|
+
let endSeq;
|
|
298
|
+
if (input.direction === "after") {
|
|
299
|
+
const cursorSeq = input.cursorSeq ?? bounds.minSeq - 1;
|
|
300
|
+
startSeq = Math.max(bounds.minSeq, cursorSeq + 1);
|
|
301
|
+
endSeq = limit === 0 ? bounds.maxSeq : Math.min(bounds.maxSeq, cursorSeq + limit);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const cursorSeq = input.cursorSeq ?? bounds.maxSeq + 1;
|
|
305
|
+
endSeq = Math.min(bounds.maxSeq, cursorSeq - 1);
|
|
306
|
+
startSeq = limit === 0 ? bounds.minSeq : Math.max(bounds.minSeq, cursorSeq - limit);
|
|
307
|
+
}
|
|
308
|
+
if (startSeq > endSeq) {
|
|
309
|
+
return {
|
|
310
|
+
entries: [],
|
|
311
|
+
startSeq: null,
|
|
312
|
+
endSeq: null,
|
|
313
|
+
hasOlder: startSeq > bounds.minSeq,
|
|
314
|
+
hasNewer: endSeq < bounds.maxSeq,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
const entries = selectEntriesOverlappingSeqRange({ entries: projectedAll, startSeq, endSeq });
|
|
318
|
+
return {
|
|
319
|
+
entries,
|
|
320
|
+
startSeq,
|
|
321
|
+
endSeq,
|
|
322
|
+
hasOlder: startSeq > bounds.minSeq,
|
|
323
|
+
hasNewer: endSeq < bounds.maxSeq,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
261
326
|
export function selectItemsByProjectedLimit(input) {
|
|
262
327
|
const rows = input.items.map((item, index) => ({
|
|
263
328
|
seq: index + 1,
|
|
@@ -199,7 +199,7 @@ export declare const PersistedConfigSchema: z.ZodObject<{
|
|
|
199
199
|
label: z.ZodString;
|
|
200
200
|
description: z.ZodOptional<z.ZodString>;
|
|
201
201
|
isDefault: z.ZodOptional<z.ZodBoolean>;
|
|
202
|
-
}, "
|
|
202
|
+
}, "strip", z.ZodTypeAny, {
|
|
203
203
|
id: string;
|
|
204
204
|
label: string;
|
|
205
205
|
description?: string | undefined;
|
|
@@ -210,7 +210,7 @@ export declare const PersistedConfigSchema: z.ZodObject<{
|
|
|
210
210
|
description?: string | undefined;
|
|
211
211
|
isDefault?: boolean | undefined;
|
|
212
212
|
}>, "many">>;
|
|
213
|
-
}, "
|
|
213
|
+
}, "strip", z.ZodTypeAny, {
|
|
214
214
|
id: string;
|
|
215
215
|
label: string;
|
|
216
216
|
description?: string | undefined;
|
|
@@ -243,7 +243,7 @@ export declare const PersistedConfigSchema: z.ZodObject<{
|
|
|
243
243
|
label: z.ZodString;
|
|
244
244
|
description: z.ZodOptional<z.ZodString>;
|
|
245
245
|
isDefault: z.ZodOptional<z.ZodBoolean>;
|
|
246
|
-
}, "
|
|
246
|
+
}, "strip", z.ZodTypeAny, {
|
|
247
247
|
id: string;
|
|
248
248
|
label: string;
|
|
249
249
|
description?: string | undefined;
|
|
@@ -254,7 +254,7 @@ export declare const PersistedConfigSchema: z.ZodObject<{
|
|
|
254
254
|
description?: string | undefined;
|
|
255
255
|
isDefault?: boolean | undefined;
|
|
256
256
|
}>, "many">>;
|
|
257
|
-
}, "
|
|
257
|
+
}, "strip", z.ZodTypeAny, {
|
|
258
258
|
id: string;
|
|
259
259
|
label: string;
|
|
260
260
|
description?: string | undefined;
|
|
@@ -280,7 +280,7 @@ export declare const PersistedConfigSchema: z.ZodObject<{
|
|
|
280
280
|
disallowedTools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
281
281
|
enabled: z.ZodOptional<z.ZodBoolean>;
|
|
282
282
|
order: z.ZodOptional<z.ZodNumber>;
|
|
283
|
-
}, "
|
|
283
|
+
}, "strip", z.ZodTypeAny, {
|
|
284
284
|
description?: string | undefined;
|
|
285
285
|
label?: string | undefined;
|
|
286
286
|
enabled?: boolean | undefined;
|
|
@@ -71,9 +71,59 @@ function parseCronExpression(expression) {
|
|
|
71
71
|
function startOfNextMinute(date) {
|
|
72
72
|
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes() + 1, 0, 0));
|
|
73
73
|
}
|
|
74
|
+
function assertValidTimeZone(timeZone) {
|
|
75
|
+
try {
|
|
76
|
+
new Intl.DateTimeFormat("en-US", { timeZone }).format(new Date(0));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new Error(`Invalid cron time zone: ${timeZone}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function createCronDatePartsReader(timeZone) {
|
|
83
|
+
if (timeZone === undefined) {
|
|
84
|
+
return (date) => ({
|
|
85
|
+
minute: date.getUTCMinutes(),
|
|
86
|
+
hour: date.getUTCHours(),
|
|
87
|
+
dayOfMonth: date.getUTCDate(),
|
|
88
|
+
month: date.getUTCMonth() + 1,
|
|
89
|
+
dayOfWeek: date.getUTCDay(),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
assertValidTimeZone(timeZone);
|
|
93
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
94
|
+
timeZone,
|
|
95
|
+
hourCycle: "h23",
|
|
96
|
+
year: "numeric",
|
|
97
|
+
month: "2-digit",
|
|
98
|
+
day: "2-digit",
|
|
99
|
+
hour: "2-digit",
|
|
100
|
+
minute: "2-digit",
|
|
101
|
+
});
|
|
102
|
+
return (date) => {
|
|
103
|
+
const values = {};
|
|
104
|
+
for (const part of formatter.formatToParts(date)) {
|
|
105
|
+
if (part.type !== "literal") {
|
|
106
|
+
values[part.type] = part.value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const year = Number.parseInt(values.year, 10);
|
|
110
|
+
const month = Number.parseInt(values.month, 10);
|
|
111
|
+
const dayOfMonth = Number.parseInt(values.day, 10);
|
|
112
|
+
return {
|
|
113
|
+
minute: Number.parseInt(values.minute, 10),
|
|
114
|
+
hour: Number.parseInt(values.hour, 10),
|
|
115
|
+
dayOfMonth,
|
|
116
|
+
month,
|
|
117
|
+
dayOfWeek: new Date(Date.UTC(year, month - 1, dayOfMonth)).getUTCDay(),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
}
|
|
74
121
|
export function validateScheduleCadence(cadence) {
|
|
75
122
|
if (cadence.type === "cron") {
|
|
76
123
|
parseCronExpression(cadence.expression);
|
|
124
|
+
if (cadence.timezone !== undefined) {
|
|
125
|
+
assertValidTimeZone(cadence.timezone);
|
|
126
|
+
}
|
|
77
127
|
}
|
|
78
128
|
}
|
|
79
129
|
export function computeNextRunAt(cadence, after) {
|
|
@@ -81,14 +131,11 @@ export function computeNextRunAt(cadence, after) {
|
|
|
81
131
|
return new Date(after.getTime() + cadence.everyMs);
|
|
82
132
|
}
|
|
83
133
|
const cron = parseCronExpression(cadence.expression);
|
|
134
|
+
const readDateParts = createCronDatePartsReader(cadence.timezone);
|
|
84
135
|
const limit = 366 * 24 * 60;
|
|
85
136
|
let cursor = startOfNextMinute(after);
|
|
86
137
|
for (let index = 0; index < limit; index += 1) {
|
|
87
|
-
const minute = cursor
|
|
88
|
-
const hour = cursor.getUTCHours();
|
|
89
|
-
const dayOfMonth = cursor.getUTCDate();
|
|
90
|
-
const month = cursor.getUTCMonth() + 1;
|
|
91
|
-
const dayOfWeek = cursor.getUTCDay();
|
|
138
|
+
const { minute, hour, dayOfMonth, month, dayOfWeek } = readDateParts(cursor);
|
|
92
139
|
if (cron.minute.matches(minute) &&
|
|
93
140
|
cron.hour.matches(hour) &&
|
|
94
141
|
cron.dayOfMonth.matches(dayOfMonth) &&
|
|
@@ -489,7 +489,10 @@ export declare class Session {
|
|
|
489
489
|
private handleWorkspaceSetupStatusRequest;
|
|
490
490
|
private handleArchiveWorkspaceRequest;
|
|
491
491
|
private handleFetchAgent;
|
|
492
|
-
private
|
|
492
|
+
private shouldUseFullTimelineForProjectedPage;
|
|
493
|
+
private selectCanonicalTimelineProjection;
|
|
494
|
+
private selectProjectedTimelineProjection;
|
|
495
|
+
private selectTimelineProjection;
|
|
493
496
|
private handleFetchAgentTimelineRequest;
|
|
494
497
|
private handleSendAgentMessageRequest;
|
|
495
498
|
private handleWaitForFinish;
|
|
@@ -38,7 +38,7 @@ import { createAgentCommand } from "./agent/create-agent/create.js";
|
|
|
38
38
|
import { archiveAgentCommand, cancelAgentRunCommand, closeAgentCommand, setAgentModeCommand, updateAgentCommand, } from "./agent/lifecycle-command.js";
|
|
39
39
|
import { buildStoredAgentPayload, resolveEffectiveThinkingOptionId, resolveStoredAgentPayloadUpdatedAt, toAgentPayload, } from "./agent/agent-projections.js";
|
|
40
40
|
import { appendTimelineItemIfAgentKnown, emitLiveTimelineItemIfAgentKnown, } from "./agent/timeline-append.js";
|
|
41
|
-
import { projectTimelineRows,
|
|
41
|
+
import { projectTimelineRows, selectProjectedTimelinePage, } from "./agent/timeline-projection.js";
|
|
42
42
|
import { StructuredAgentFallbackError, StructuredAgentResponseError, generateStructuredAgentResponseWithFallback, } from "./agent/agent-response-loop.js";
|
|
43
43
|
import { resolveStructuredGenerationProviders, } from "./agent/structured-generation-providers.js";
|
|
44
44
|
import { getAgentStreamEventTurnId, } from "./agent/agent-sdk-types.js";
|
|
@@ -370,6 +370,7 @@ export class Session {
|
|
|
370
370
|
hasBinaryChannel: () => this.onBinaryMessage !== null,
|
|
371
371
|
isPathWithinRoot: (rootPath, candidatePath) => this.isPathWithinRoot(rootPath, candidatePath),
|
|
372
372
|
sessionLogger: this.sessionLogger,
|
|
373
|
+
clientSupportsWrapReflow: () => this.clientCapabilities.has(CLIENT_CAPS.terminalReflowableSnapshot),
|
|
373
374
|
});
|
|
374
375
|
this.createAgentLifecycleDispatch = new CreateAgentLifecycleDispatch({
|
|
375
376
|
paseoHome: this.paseoHome,
|
|
@@ -5652,62 +5653,63 @@ export class Session {
|
|
|
5652
5653
|
payload: { requestId, agent, project, error: null },
|
|
5653
5654
|
});
|
|
5654
5655
|
}
|
|
5655
|
-
|
|
5656
|
-
const {
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5656
|
+
shouldUseFullTimelineForProjectedPage(input) {
|
|
5657
|
+
const { timeline } = input;
|
|
5658
|
+
if (timeline.reset || timeline.rows.length === 0 || !timeline.hasOlder) {
|
|
5659
|
+
return false;
|
|
5660
|
+
}
|
|
5661
|
+
const firstRow = timeline.rows[0];
|
|
5662
|
+
if (firstRow?.item.type === "assistant_message" ||
|
|
5663
|
+
firstRow?.item.type === "reasoning" ||
|
|
5664
|
+
firstRow?.item.type === "tool_call") {
|
|
5665
|
+
return true;
|
|
5666
|
+
}
|
|
5667
|
+
return timeline.rows.some((row) => row.item.type === "tool_call");
|
|
5668
|
+
}
|
|
5669
|
+
selectCanonicalTimelineProjection(input) {
|
|
5670
|
+
const entries = projectTimelineRows({ rows: input.timeline.rows, mode: "canonical" });
|
|
5671
|
+
return {
|
|
5672
|
+
timeline: input.timeline,
|
|
5673
|
+
entries,
|
|
5674
|
+
startSeq: entries[0]?.seqStart ?? null,
|
|
5675
|
+
endSeq: entries[entries.length - 1]?.seqEnd ?? null,
|
|
5676
|
+
hasOlder: input.timeline.hasOlder,
|
|
5677
|
+
hasNewer: input.timeline.hasNewer,
|
|
5678
|
+
};
|
|
5679
|
+
}
|
|
5680
|
+
selectProjectedTimelineProjection(input) {
|
|
5681
|
+
const timeline = this.shouldUseFullTimelineForProjectedPage({
|
|
5682
|
+
timeline: input.controlTimeline,
|
|
5683
|
+
})
|
|
5684
|
+
? this.agentManager.fetchTimeline(input.agentId, { direction: "tail", limit: 0 })
|
|
5685
|
+
: input.controlTimeline;
|
|
5686
|
+
const page = selectProjectedTimelinePage({
|
|
5661
5687
|
rows: timeline.rows,
|
|
5662
|
-
|
|
5663
|
-
|
|
5664
|
-
|
|
5688
|
+
bounds: timeline.window,
|
|
5689
|
+
direction: input.controlTimeline.reset ? "tail" : input.direction,
|
|
5690
|
+
...(input.cursor ? { cursorSeq: input.cursor.seq } : {}),
|
|
5691
|
+
limit: input.pageLimit,
|
|
5665
5692
|
});
|
|
5666
|
-
while (timeline.hasOlder) {
|
|
5667
|
-
const needsMoreProjectedEntries = projectedWindow.projectedEntries.length < projectedLimit;
|
|
5668
|
-
const firstLoadedRow = timeline.rows[0];
|
|
5669
|
-
const firstSelectedRow = projectedWindow.selectedRows[0];
|
|
5670
|
-
const startsAtLoadedBoundary = firstLoadedRow != null &&
|
|
5671
|
-
firstSelectedRow != null &&
|
|
5672
|
-
firstSelectedRow.seq === firstLoadedRow.seq;
|
|
5673
|
-
const boundaryIsAssistantChunk = startsAtLoadedBoundary && firstLoadedRow.item.type === "assistant_message";
|
|
5674
|
-
if (!needsMoreProjectedEntries && !boundaryIsAssistantChunk) {
|
|
5675
|
-
break;
|
|
5676
|
-
}
|
|
5677
|
-
const maxRows = Math.max(0, timeline.window.maxSeq - timeline.window.minSeq + 1);
|
|
5678
|
-
const nextFetchLimit = Math.min(maxRows, fetchLimit * 2);
|
|
5679
|
-
if (nextFetchLimit <= fetchLimit) {
|
|
5680
|
-
break;
|
|
5681
|
-
}
|
|
5682
|
-
fetchLimit = nextFetchLimit;
|
|
5683
|
-
timeline = this.agentManager.fetchTimeline(agentId, {
|
|
5684
|
-
direction,
|
|
5685
|
-
cursor,
|
|
5686
|
-
limit: fetchLimit,
|
|
5687
|
-
});
|
|
5688
|
-
projectedWindow = selectTimelineWindowByProjectedLimit({
|
|
5689
|
-
rows: timeline.rows,
|
|
5690
|
-
direction,
|
|
5691
|
-
limit: projectedLimit,
|
|
5692
|
-
collapseToolLifecycle: false,
|
|
5693
|
-
});
|
|
5694
|
-
}
|
|
5695
5693
|
return {
|
|
5696
5694
|
timeline,
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5695
|
+
entries: page.entries,
|
|
5696
|
+
startSeq: page.startSeq,
|
|
5697
|
+
endSeq: page.endSeq,
|
|
5698
|
+
hasOlder: page.hasOlder || (page.startSeq !== null && page.startSeq > timeline.window.minSeq),
|
|
5699
|
+
hasNewer: page.hasNewer,
|
|
5700
5700
|
};
|
|
5701
5701
|
}
|
|
5702
|
+
selectTimelineProjection(input) {
|
|
5703
|
+
if (input.projection === "canonical") {
|
|
5704
|
+
return this.selectCanonicalTimelineProjection({ timeline: input.controlTimeline });
|
|
5705
|
+
}
|
|
5706
|
+
return this.selectProjectedTimelineProjection(input);
|
|
5707
|
+
}
|
|
5702
5708
|
async handleFetchAgentTimelineRequest(msg) {
|
|
5703
5709
|
const direction = msg.direction ?? (msg.cursor ? "after" : "tail");
|
|
5704
5710
|
const projection = msg.projection ?? "projected";
|
|
5705
5711
|
const requestedLimit = msg.limit;
|
|
5706
|
-
const
|
|
5707
|
-
const shouldLimitByProjectedWindow = projection === "canonical" &&
|
|
5708
|
-
direction === "tail" &&
|
|
5709
|
-
typeof requestedLimit === "number" &&
|
|
5710
|
-
requestedLimit > 0;
|
|
5712
|
+
const pageLimit = requestedLimit ?? (direction === "after" ? 0 : 200);
|
|
5711
5713
|
const cursor = msg.cursor
|
|
5712
5714
|
? {
|
|
5713
5715
|
epoch: msg.cursor.epoch,
|
|
@@ -5721,42 +5723,25 @@ export class Session {
|
|
|
5721
5723
|
logger: this.sessionLogger,
|
|
5722
5724
|
});
|
|
5723
5725
|
const agentPayload = await this.buildAgentPayload(snapshot);
|
|
5724
|
-
|
|
5726
|
+
const controlTimeline = this.agentManager.fetchTimeline(msg.agentId, {
|
|
5725
5727
|
direction,
|
|
5726
5728
|
cursor,
|
|
5727
|
-
limit:
|
|
5728
|
-
|
|
5729
|
-
|
|
5730
|
-
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
|
|
5735
|
-
|
|
5736
|
-
|
|
5737
|
-
|
|
5738
|
-
|
|
5739
|
-
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
});
|
|
5744
|
-
timeline = projectedResult.timeline;
|
|
5745
|
-
entries = projectTimelineRows({ rows: projectedResult.selectedRows, mode: projection });
|
|
5746
|
-
if (projectedResult.minSeq !== null && projectedResult.maxSeq !== null) {
|
|
5747
|
-
startCursor = { epoch: timeline.epoch, seq: projectedResult.minSeq };
|
|
5748
|
-
endCursor = { epoch: timeline.epoch, seq: projectedResult.maxSeq };
|
|
5749
|
-
hasOlder = projectedResult.minSeq > timeline.window.minSeq;
|
|
5750
|
-
hasNewer = false;
|
|
5751
|
-
}
|
|
5752
|
-
}
|
|
5753
|
-
else {
|
|
5754
|
-
const firstRow = timeline.rows[0];
|
|
5755
|
-
const lastRow = timeline.rows[timeline.rows.length - 1];
|
|
5756
|
-
startCursor = firstRow ? { epoch: timeline.epoch, seq: firstRow.seq } : null;
|
|
5757
|
-
endCursor = lastRow ? { epoch: timeline.epoch, seq: lastRow.seq } : null;
|
|
5758
|
-
entries = projectTimelineRows({ rows: timeline.rows, mode: projection });
|
|
5759
|
-
}
|
|
5729
|
+
limit: pageLimit,
|
|
5730
|
+
});
|
|
5731
|
+
const selectedTimeline = this.selectTimelineProjection({
|
|
5732
|
+
agentId: msg.agentId,
|
|
5733
|
+
projection,
|
|
5734
|
+
controlTimeline,
|
|
5735
|
+
direction,
|
|
5736
|
+
...(cursor ? { cursor } : {}),
|
|
5737
|
+
pageLimit,
|
|
5738
|
+
});
|
|
5739
|
+
const startCursor = selectedTimeline.startSeq !== null
|
|
5740
|
+
? { epoch: selectedTimeline.timeline.epoch, seq: selectedTimeline.startSeq }
|
|
5741
|
+
: null;
|
|
5742
|
+
const endCursor = selectedTimeline.endSeq !== null
|
|
5743
|
+
? { epoch: selectedTimeline.timeline.epoch, seq: selectedTimeline.endSeq }
|
|
5744
|
+
: null;
|
|
5760
5745
|
this.emit({
|
|
5761
5746
|
type: "fetch_agent_timeline_response",
|
|
5762
5747
|
payload: {
|
|
@@ -5765,16 +5750,16 @@ export class Session {
|
|
|
5765
5750
|
agent: agentPayload,
|
|
5766
5751
|
direction,
|
|
5767
5752
|
projection,
|
|
5768
|
-
epoch: timeline.epoch,
|
|
5769
|
-
reset:
|
|
5770
|
-
staleCursor:
|
|
5771
|
-
gap:
|
|
5772
|
-
window: timeline.window,
|
|
5753
|
+
epoch: selectedTimeline.timeline.epoch,
|
|
5754
|
+
reset: controlTimeline.reset,
|
|
5755
|
+
staleCursor: controlTimeline.staleCursor,
|
|
5756
|
+
gap: controlTimeline.gap,
|
|
5757
|
+
window: selectedTimeline.timeline.window,
|
|
5773
5758
|
startCursor,
|
|
5774
5759
|
endCursor,
|
|
5775
|
-
hasOlder,
|
|
5776
|
-
hasNewer,
|
|
5777
|
-
entries: entries.map((entry) => ({
|
|
5760
|
+
hasOlder: selectedTimeline.hasOlder,
|
|
5761
|
+
hasNewer: selectedTimeline.hasNewer,
|
|
5762
|
+
entries: selectedTimeline.entries.map((entry) => ({
|
|
5778
5763
|
provider: snapshot.provider,
|
|
5779
5764
|
item: entry.item,
|
|
5780
5765
|
timestamp: entry.timestamp,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createTerminal, } from "./terminal.js";
|
|
2
2
|
import { captureTerminalLines } from "./terminal-capture.js";
|
|
3
3
|
import { resolve, sep, win32, posix } from "node:path";
|
|
4
|
+
import { isSameOrDescendantPath } from "../server/path-utils.js";
|
|
4
5
|
export function createTerminalManager() {
|
|
5
6
|
const terminalsByCwd = new Map();
|
|
6
7
|
const terminalsById = new Map();
|
|
@@ -99,7 +100,16 @@ export function createTerminalManager() {
|
|
|
99
100
|
return {
|
|
100
101
|
async getTerminals(cwd) {
|
|
101
102
|
assertAbsolutePath(cwd);
|
|
102
|
-
|
|
103
|
+
// Terminals are bucketed by exact cwd, but an agent can open a terminal in
|
|
104
|
+
// a subdirectory of the workspace. A query for the workspace root must
|
|
105
|
+
// surface those too, so aggregate every bucket at or below `cwd`.
|
|
106
|
+
const sessions = [];
|
|
107
|
+
for (const [bucketCwd, bucketSessions] of terminalsByCwd) {
|
|
108
|
+
if (isSameOrDescendantPath(cwd, bucketCwd)) {
|
|
109
|
+
sessions.push(...bucketSessions);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return sessions;
|
|
103
113
|
},
|
|
104
114
|
async createTerminal(options) {
|
|
105
115
|
assertAbsolutePath(options.cwd);
|
|
@@ -9,6 +9,7 @@ export interface TerminalSessionControllerOptions {
|
|
|
9
9
|
hasBinaryChannel: () => boolean;
|
|
10
10
|
isPathWithinRoot: (rootPath: string, candidatePath: string) => boolean;
|
|
11
11
|
sessionLogger: pino.Logger;
|
|
12
|
+
clientSupportsWrapReflow?: () => boolean;
|
|
12
13
|
}
|
|
13
14
|
export interface TerminalSessionControllerMetrics {
|
|
14
15
|
directorySubscriptionCount: number;
|
|
@@ -21,6 +22,7 @@ export declare class TerminalSessionController {
|
|
|
21
22
|
private readonly hasBinaryChannel;
|
|
22
23
|
private readonly isPathWithinRoot;
|
|
23
24
|
private readonly sessionLogger;
|
|
25
|
+
private readonly clientSupportsWrapReflow;
|
|
24
26
|
private readonly subscribedDirectories;
|
|
25
27
|
private unsubscribeTerminalsChanged;
|
|
26
28
|
private readonly exitSubscriptions;
|
|
@@ -45,7 +47,7 @@ export declare class TerminalSessionController {
|
|
|
45
47
|
private handleTerminalsChanged;
|
|
46
48
|
private handleSubscribeTerminalsRequest;
|
|
47
49
|
private handleUnsubscribeTerminalsRequest;
|
|
48
|
-
private
|
|
50
|
+
private emitTerminalsSnapshotForRoot;
|
|
49
51
|
private handleListTerminalsRequest;
|
|
50
52
|
private getAllTerminalSessions;
|
|
51
53
|
private handleCreateTerminalRequest;
|
|
@@ -29,12 +29,15 @@ export class TerminalSessionController {
|
|
|
29
29
|
this.hasBinaryChannel = options.hasBinaryChannel;
|
|
30
30
|
this.isPathWithinRoot = options.isPathWithinRoot;
|
|
31
31
|
this.sessionLogger = options.sessionLogger;
|
|
32
|
+
this.clientSupportsWrapReflow = options.clientSupportsWrapReflow ?? (() => false);
|
|
32
33
|
}
|
|
33
34
|
start() {
|
|
34
35
|
if (!this.terminalManager) {
|
|
35
36
|
return;
|
|
36
37
|
}
|
|
37
|
-
this.unsubscribeTerminalsChanged = this.terminalManager.subscribeTerminalsChanged((event) =>
|
|
38
|
+
this.unsubscribeTerminalsChanged = this.terminalManager.subscribeTerminalsChanged((event) => {
|
|
39
|
+
void this.handleTerminalsChanged(event);
|
|
40
|
+
});
|
|
38
41
|
}
|
|
39
42
|
getMetrics() {
|
|
40
43
|
return {
|
|
@@ -173,23 +176,25 @@ export class TerminalSessionController {
|
|
|
173
176
|
...(title ? { title } : {}),
|
|
174
177
|
};
|
|
175
178
|
}
|
|
176
|
-
handleTerminalsChanged(event) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
async handleTerminalsChanged(event) {
|
|
180
|
+
// A terminal can live in a subdirectory of a subscribed workspace root (an
|
|
181
|
+
// agent can open one there). Deliver the change to every subscribed root at
|
|
182
|
+
// or above the terminal's cwd, keyed by that root, carrying the full
|
|
183
|
+
// aggregated list — so the client's cache replacement doesn't drop the
|
|
184
|
+
// terminals that live directly at the root.
|
|
185
|
+
const matchingRoots = Array.from(this.subscribedDirectories).filter((root) => this.isPathWithinRoot(root, event.cwd));
|
|
186
|
+
for (const root of matchingRoots) {
|
|
187
|
+
await this.emitTerminalsSnapshotForRoot(root);
|
|
179
188
|
}
|
|
180
|
-
this.emitTerminalsChangedSnapshot({
|
|
181
|
-
cwd: event.cwd,
|
|
182
|
-
terminals: event.terminals.map((terminal) => Object.assign({ id: terminal.id, name: terminal.name }, terminal.title ? { title: terminal.title } : {})),
|
|
183
|
-
});
|
|
184
189
|
}
|
|
185
190
|
handleSubscribeTerminalsRequest(msg) {
|
|
186
191
|
this.subscribedDirectories.add(msg.cwd);
|
|
187
|
-
void this.
|
|
192
|
+
void this.emitTerminalsSnapshotForRoot(msg.cwd);
|
|
188
193
|
}
|
|
189
194
|
handleUnsubscribeTerminalsRequest(msg) {
|
|
190
195
|
this.subscribedDirectories.delete(msg.cwd);
|
|
191
196
|
}
|
|
192
|
-
async
|
|
197
|
+
async emitTerminalsSnapshotForRoot(cwd) {
|
|
193
198
|
if (!this.terminalManager || !this.subscribedDirectories.has(cwd)) {
|
|
194
199
|
return;
|
|
195
200
|
}
|
|
@@ -618,7 +623,9 @@ export class TerminalSessionController {
|
|
|
618
623
|
}
|
|
619
624
|
}
|
|
620
625
|
async emitLegacySnapshot(activeStream, terminalManager) {
|
|
621
|
-
const snapshot = await terminalManager.getTerminalState(activeStream.terminalId
|
|
626
|
+
const snapshot = await terminalManager.getTerminalState(activeStream.terminalId, {
|
|
627
|
+
includeWrapFlags: this.clientSupportsWrapReflow(),
|
|
628
|
+
});
|
|
622
629
|
if (this.activeStreams.get(activeStream.slot) !== activeStream) {
|
|
623
630
|
return { shouldContinue: false };
|
|
624
631
|
}
|
|
@@ -637,7 +644,10 @@ export class TerminalSessionController {
|
|
|
637
644
|
if (snapshotOptions === null) {
|
|
638
645
|
return { shouldContinue: true };
|
|
639
646
|
}
|
|
640
|
-
const snapshot = await terminalManager.getTerminalState(activeStream.terminalId,
|
|
647
|
+
const snapshot = await terminalManager.getTerminalState(activeStream.terminalId, {
|
|
648
|
+
...snapshotOptions,
|
|
649
|
+
includeWrapFlags: this.clientSupportsWrapReflow(),
|
|
650
|
+
});
|
|
641
651
|
if (this.activeStreams.get(activeStream.slot) !== activeStream) {
|
|
642
652
|
return { shouldContinue: false };
|
|
643
653
|
}
|
|
@@ -232,6 +232,32 @@ function extractScrollback(terminal, options) {
|
|
|
232
232
|
}
|
|
233
233
|
return scrollback;
|
|
234
234
|
}
|
|
235
|
+
// xterm marks a line `isWrapped` when it is a continuation of the PREVIOUS line.
|
|
236
|
+
// The snapshot carries the inverse, tmux-style flag — "this row continues onto the
|
|
237
|
+
// next row" — so the client can rejoin and reflow logical lines. So row y's flag is
|
|
238
|
+
// whether line y+1 is a wrapped continuation.
|
|
239
|
+
function lineContinuesToNext(terminal, absoluteRow) {
|
|
240
|
+
return terminal.buffer.active.getLine(absoluteRow + 1)?.isWrapped === true;
|
|
241
|
+
}
|
|
242
|
+
function extractGridWrapped(terminal) {
|
|
243
|
+
const baseY = terminal.buffer.active.baseY;
|
|
244
|
+
const wrapped = [];
|
|
245
|
+
for (let row = 0; row < terminal.rows; row++) {
|
|
246
|
+
wrapped.push(lineContinuesToNext(terminal, baseY + row));
|
|
247
|
+
}
|
|
248
|
+
return wrapped;
|
|
249
|
+
}
|
|
250
|
+
function extractScrollbackWrapped(terminal, options) {
|
|
251
|
+
const scrollbackLines = terminal.buffer.active.baseY;
|
|
252
|
+
const startRow = typeof options?.scrollbackLines === "number"
|
|
253
|
+
? Math.max(0, scrollbackLines - options.scrollbackLines)
|
|
254
|
+
: 0;
|
|
255
|
+
const wrapped = [];
|
|
256
|
+
for (let row = startRow; row < scrollbackLines; row++) {
|
|
257
|
+
wrapped.push(lineContinuesToNext(terminal, row));
|
|
258
|
+
}
|
|
259
|
+
return wrapped;
|
|
260
|
+
}
|
|
235
261
|
function extractCursorState(terminal) {
|
|
236
262
|
const coreService = terminal
|
|
237
263
|
._core?.coreService;
|
|
@@ -659,6 +685,14 @@ export async function createTerminal(options) {
|
|
|
659
685
|
}),
|
|
660
686
|
cursor: extractCursorState(terminal),
|
|
661
687
|
...(title ? { title } : {}),
|
|
688
|
+
...(snapshotOptions?.includeWrapFlags
|
|
689
|
+
? {
|
|
690
|
+
gridWrapped: extractGridWrapped(terminal),
|
|
691
|
+
scrollbackWrapped: extractScrollbackWrapped(terminal, {
|
|
692
|
+
scrollbackLines: snapshotOptions?.scrollbackLines,
|
|
693
|
+
}),
|
|
694
|
+
}
|
|
695
|
+
: {}),
|
|
662
696
|
};
|
|
663
697
|
}
|
|
664
698
|
function getStateSnapshot(snapshotOptions) {
|
|
@@ -375,7 +375,7 @@ function buildGitDiffArgs(args) {
|
|
|
375
375
|
return ["diff", ...(args.ignoreWhitespace ? ["-w"] : []), ...args.extra];
|
|
376
376
|
}
|
|
377
377
|
const TRACKED_DIFF_NUMSTAT_MAX_BYTES = 2 * 1024 * 1024; // 2MB
|
|
378
|
-
const
|
|
378
|
+
const TRACKED_DIFF_PER_FILE_MAX_CHARS = 1024 * 1024;
|
|
379
379
|
const EMPTY_TREE_OBJECT_ID = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
380
380
|
function isUnbornHeadDiffError(error) {
|
|
381
381
|
return (error instanceof Error &&
|
|
@@ -423,11 +423,56 @@ async function getTrackedNumstatByPath(cwd, refs, ignoreWhitespace = false) {
|
|
|
423
423
|
}
|
|
424
424
|
return stats;
|
|
425
425
|
}
|
|
426
|
-
function
|
|
427
|
-
|
|
428
|
-
|
|
426
|
+
function extractTrackedDiffMetadataPath(section, prefix) {
|
|
427
|
+
const line = section.split("\n").find((candidate) => candidate.startsWith(prefix));
|
|
428
|
+
if (!line) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
const path = line.slice(prefix.length).replace(/\t.*$/, "").trimEnd();
|
|
432
|
+
if (path === "/dev/null") {
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
return path.startsWith("a/") || path.startsWith("b/") ? path.slice(2) : path;
|
|
436
|
+
}
|
|
437
|
+
function extractTrackedDiffSectionPath(section) {
|
|
438
|
+
const firstLineEnd = section.indexOf("\n");
|
|
439
|
+
const firstLine = firstLineEnd === -1 ? section : section.slice(0, firstLineEnd);
|
|
440
|
+
const header = firstLine.startsWith("diff --git ") ? firstLine.slice("diff --git ".length) : "";
|
|
441
|
+
const prefixedPathMatch = header.match(/^a\/(.+) b\/(.+)$/);
|
|
442
|
+
if (prefixedPathMatch) {
|
|
443
|
+
return prefixedPathMatch[2] ?? null;
|
|
444
|
+
}
|
|
445
|
+
const metadataPath = extractTrackedDiffMetadataPath(section, "+++ ") ??
|
|
446
|
+
extractTrackedDiffMetadataPath(section, "--- ");
|
|
447
|
+
if (metadataPath) {
|
|
448
|
+
return metadataPath;
|
|
449
|
+
}
|
|
450
|
+
const pathMatch = header.match(/^(\S+)\s+(\S+)$/);
|
|
451
|
+
return pathMatch?.[2] ?? null;
|
|
452
|
+
}
|
|
453
|
+
function splitTrackedDiffSections(diffText) {
|
|
454
|
+
const starts = [];
|
|
455
|
+
const diffHeaderPattern = /^diff --git /gm;
|
|
456
|
+
let match;
|
|
457
|
+
while ((match = diffHeaderPattern.exec(diffText))) {
|
|
458
|
+
starts.push(match.index);
|
|
459
|
+
}
|
|
460
|
+
const sections = [];
|
|
461
|
+
for (let index = 0; index < starts.length; index += 1) {
|
|
462
|
+
const start = starts[index];
|
|
463
|
+
const end = starts[index + 1] ?? diffText.length;
|
|
464
|
+
const text = diffText.slice(start, end);
|
|
465
|
+
const path = extractTrackedDiffSectionPath(text);
|
|
466
|
+
if (!path) {
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
sections.push({
|
|
470
|
+
path,
|
|
471
|
+
text,
|
|
472
|
+
isTooLarge: text.length > TRACKED_DIFF_PER_FILE_MAX_CHARS,
|
|
473
|
+
});
|
|
429
474
|
}
|
|
430
|
-
return
|
|
475
|
+
return sections;
|
|
431
476
|
}
|
|
432
477
|
export class NotGitRepoError extends Error {
|
|
433
478
|
constructor(cwd) {
|
|
@@ -1500,6 +1545,62 @@ async function processUntrackedChange(input) {
|
|
|
1500
1545
|
status: "ok",
|
|
1501
1546
|
});
|
|
1502
1547
|
}
|
|
1548
|
+
async function processTrackedChanges(input) {
|
|
1549
|
+
const { cwd, refsForDiff, trackedChanges, ignoreWhitespace, appendDiff } = input;
|
|
1550
|
+
const trackedChangeByPath = new Map(trackedChanges.map((change) => [change.path, change]));
|
|
1551
|
+
const trackedNumstatByPath = trackedChanges.length > 0
|
|
1552
|
+
? await getTrackedNumstatByPath(cwd, refsForDiff, ignoreWhitespace)
|
|
1553
|
+
: new Map();
|
|
1554
|
+
const trackedDiffPaths = [];
|
|
1555
|
+
const trackedPlaceholderByPath = new Map();
|
|
1556
|
+
for (const change of trackedChanges) {
|
|
1557
|
+
const stat = trackedNumstatByPath.get(change.path) ?? null;
|
|
1558
|
+
if (stat?.isBinary) {
|
|
1559
|
+
trackedPlaceholderByPath.set(change.path, { status: "binary", stat });
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
trackedDiffPaths.push(change.path);
|
|
1563
|
+
}
|
|
1564
|
+
let trackedDiffText = "";
|
|
1565
|
+
let trackedDiffTruncated = false;
|
|
1566
|
+
if (trackedDiffPaths.length > 0) {
|
|
1567
|
+
const trackedDiffResult = await runGitCommand(buildGitDiffArgs({
|
|
1568
|
+
ignoreWhitespace,
|
|
1569
|
+
extra: [...getCheckoutDiffRefArgs(refsForDiff), "--", ...trackedDiffPaths],
|
|
1570
|
+
}), {
|
|
1571
|
+
cwd,
|
|
1572
|
+
envOverlay: READ_ONLY_GIT_ENV,
|
|
1573
|
+
maxOutputBytes: TOTAL_DIFF_MAX_BYTES,
|
|
1574
|
+
});
|
|
1575
|
+
trackedDiffTruncated = trackedDiffResult.truncated;
|
|
1576
|
+
const visibleTrackedDiffs = [];
|
|
1577
|
+
const sections = splitTrackedDiffSections(trackedDiffResult.stdout);
|
|
1578
|
+
for (let index = 0; index < sections.length; index += 1) {
|
|
1579
|
+
const section = sections[index];
|
|
1580
|
+
const isTruncatedTail = trackedDiffTruncated && index === sections.length - 1;
|
|
1581
|
+
if (section.isTooLarge || isTruncatedTail) {
|
|
1582
|
+
trackedPlaceholderByPath.set(section.path, {
|
|
1583
|
+
status: "too_large",
|
|
1584
|
+
stat: trackedNumstatByPath.get(section.path) ?? null,
|
|
1585
|
+
});
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
visibleTrackedDiffs.push(section.text);
|
|
1589
|
+
}
|
|
1590
|
+
trackedDiffText = visibleTrackedDiffs.join("");
|
|
1591
|
+
appendDiff(trackedDiffText);
|
|
1592
|
+
if (trackedDiffTruncated) {
|
|
1593
|
+
appendDiff("# tracked diff truncated\n");
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
return {
|
|
1597
|
+
trackedChangeByPath,
|
|
1598
|
+
trackedNumstatByPath,
|
|
1599
|
+
trackedPlaceholderByPath,
|
|
1600
|
+
trackedDiffText,
|
|
1601
|
+
trackedDiffTruncated,
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1503
1604
|
async function resolveCheckoutDiffRefs(cwd, compare, context) {
|
|
1504
1605
|
if (compare.mode === "uncommitted") {
|
|
1505
1606
|
return { baseRef: "HEAD", includeUntracked: true };
|
|
@@ -1565,42 +1666,13 @@ export async function getCheckoutDiff(cwd, compare, context) {
|
|
|
1565
1666
|
};
|
|
1566
1667
|
const trackedChanges = changes.filter((change) => !change.isUntracked);
|
|
1567
1668
|
const untrackedChanges = changes.filter((change) => change.isUntracked === true);
|
|
1568
|
-
const
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
const stat = trackedNumstatByPath.get(change.path) ?? null;
|
|
1576
|
-
if (stat?.isBinary) {
|
|
1577
|
-
trackedPlaceholderByPath.set(change.path, { status: "binary", stat });
|
|
1578
|
-
continue;
|
|
1579
|
-
}
|
|
1580
|
-
if (isTrackedDiffTooLarge(stat)) {
|
|
1581
|
-
trackedPlaceholderByPath.set(change.path, { status: "too_large", stat });
|
|
1582
|
-
continue;
|
|
1583
|
-
}
|
|
1584
|
-
trackedDiffPaths.push(change.path);
|
|
1585
|
-
}
|
|
1586
|
-
let trackedDiffText = "";
|
|
1587
|
-
let trackedDiffTruncated = false;
|
|
1588
|
-
if (trackedDiffPaths.length > 0) {
|
|
1589
|
-
const trackedDiffResult = await runGitCommand(buildGitDiffArgs({
|
|
1590
|
-
ignoreWhitespace,
|
|
1591
|
-
extra: [...getCheckoutDiffRefArgs(effectiveRefsForDiff), "--", ...trackedDiffPaths],
|
|
1592
|
-
}), {
|
|
1593
|
-
cwd,
|
|
1594
|
-
envOverlay: READ_ONLY_GIT_ENV,
|
|
1595
|
-
maxOutputBytes: TOTAL_DIFF_MAX_BYTES,
|
|
1596
|
-
});
|
|
1597
|
-
trackedDiffText = trackedDiffResult.stdout;
|
|
1598
|
-
trackedDiffTruncated = trackedDiffResult.truncated;
|
|
1599
|
-
appendDiff(trackedDiffText);
|
|
1600
|
-
if (trackedDiffTruncated) {
|
|
1601
|
-
appendDiff("# tracked diff truncated\n");
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1669
|
+
const trackedDiff = await processTrackedChanges({
|
|
1670
|
+
cwd,
|
|
1671
|
+
refsForDiff: effectiveRefsForDiff,
|
|
1672
|
+
trackedChanges,
|
|
1673
|
+
ignoreWhitespace,
|
|
1674
|
+
appendDiff,
|
|
1675
|
+
});
|
|
1604
1676
|
const appendTrackedPlaceholderComment = (change, status) => {
|
|
1605
1677
|
if (status === "binary") {
|
|
1606
1678
|
appendDiff(`# ${change.path}: binary diff omitted\n`);
|
|
@@ -1612,11 +1684,11 @@ export async function getCheckoutDiff(cwd, compare, context) {
|
|
|
1612
1684
|
await appendStructuredTrackedDiffs({
|
|
1613
1685
|
cwd,
|
|
1614
1686
|
trackedChanges,
|
|
1615
|
-
trackedChangeByPath,
|
|
1616
|
-
trackedNumstatByPath,
|
|
1617
|
-
trackedPlaceholderByPath,
|
|
1618
|
-
trackedDiffText,
|
|
1619
|
-
trackedDiffTruncated,
|
|
1687
|
+
trackedChangeByPath: trackedDiff.trackedChangeByPath,
|
|
1688
|
+
trackedNumstatByPath: trackedDiff.trackedNumstatByPath,
|
|
1689
|
+
trackedPlaceholderByPath: trackedDiff.trackedPlaceholderByPath,
|
|
1690
|
+
trackedDiffText: trackedDiff.trackedDiffText,
|
|
1691
|
+
trackedDiffTruncated: trackedDiff.trackedDiffTruncated,
|
|
1620
1692
|
refsForDiff: effectiveRefsForDiff,
|
|
1621
1693
|
ignoreWhitespace,
|
|
1622
1694
|
structured,
|
|
@@ -1626,7 +1698,7 @@ export async function getCheckoutDiff(cwd, compare, context) {
|
|
|
1626
1698
|
}
|
|
1627
1699
|
else {
|
|
1628
1700
|
for (const change of trackedChanges) {
|
|
1629
|
-
const placeholder = trackedPlaceholderByPath.get(change.path);
|
|
1701
|
+
const placeholder = trackedDiff.trackedPlaceholderByPath.get(change.path);
|
|
1630
1702
|
if (placeholder) {
|
|
1631
1703
|
appendTrackedPlaceholderComment(change, placeholder.status);
|
|
1632
1704
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getpaseo/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.88",
|
|
4
4
|
"description": "Paseo backend server",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist/server",
|
|
@@ -57,10 +57,10 @@
|
|
|
57
57
|
"dependencies": {
|
|
58
58
|
"@agentclientprotocol/sdk": "^0.17.1",
|
|
59
59
|
"@anthropic-ai/claude-agent-sdk": "^0.2.133",
|
|
60
|
-
"@getpaseo/client": "0.1.
|
|
61
|
-
"@getpaseo/highlight": "0.1.
|
|
62
|
-
"@getpaseo/protocol": "0.1.
|
|
63
|
-
"@getpaseo/relay": "0.1.
|
|
60
|
+
"@getpaseo/client": "0.1.88",
|
|
61
|
+
"@getpaseo/highlight": "0.1.88",
|
|
62
|
+
"@getpaseo/protocol": "0.1.88",
|
|
63
|
+
"@getpaseo/relay": "0.1.88",
|
|
64
64
|
"@isaacs/ttlcache": "^2.1.4",
|
|
65
65
|
"@modelcontextprotocol/sdk": "^1.20.1",
|
|
66
66
|
"@opencode-ai/sdk": "1.14.46",
|