u-foo 2.5.14 → 3.0.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/package.json +1 -1
- package/src/agents/prompts/native/environment.js +20 -8
- package/src/code/agent.js +517 -112
- package/src/code/commands.js +77 -0
- package/src/code/context/artifactGc.js +292 -0
- package/src/code/context/artifactIndex.js +161 -0
- package/src/code/context/artifacts.js +183 -0
- package/src/code/context/assembler.js +703 -0
- package/src/code/context/executionSegment.js +292 -0
- package/src/code/context/index.js +28 -0
- package/src/code/context/planGraph.js +1410 -0
- package/src/code/context/planGraphService.js +857 -0
- package/src/code/context/planMode.js +398 -0
- package/src/code/context/planProjection.js +432 -0
- package/src/code/context/projectSnapshot.js +201 -0
- package/src/code/context/promptLayers.js +175 -0
- package/src/code/context/reducers.js +328 -0
- package/src/code/context/stableJson.js +29 -0
- package/src/code/context/stateCommit.js +414 -0
- package/src/code/context/toolRuntime.js +172 -0
- package/src/code/context/transcript.js +182 -0
- package/src/code/context/transcriptSync.js +106 -0
- package/src/code/context/userInteraction.js +457 -0
- package/src/code/context/userNudge.js +116 -0
- package/src/code/context/workingSet.js +323 -0
- package/src/code/dispatch.js +20 -1
- package/src/code/index.js +8 -0
- package/src/code/modelCommand.js +87 -0
- package/src/code/nativeRunner.js +625 -34
- package/src/code/repl.js +196 -50
- package/src/code/runtime/agentWakeup.js +58 -0
- package/src/code/runtime/graphOwner.js +41 -0
- package/src/code/runtime/graphYieldRouter.js +42 -0
- package/src/code/runtime/index.js +15 -0
- package/src/code/runtime/loopMailbox.js +124 -0
- package/src/code/runtime/runtimeEvents.js +39 -0
- package/src/code/runtime/taskControl.js +565 -0
- package/src/code/runtime/taskFocus.js +165 -0
- package/src/code/runtime/taskLoop.js +383 -0
- package/src/code/runtime/taskRun.js +187 -0
- package/src/code/runtime/toolProvenance.js +70 -0
- package/src/code/runtime/workspaceLease.js +208 -0
- package/src/code/sessionStore.js +217 -15
- package/src/code/skills/index.js +10 -0
- package/src/code/skills/injection.js +66 -3
- package/src/code/skills/loader.js +21 -0
- package/src/code/skills/manifest.js +87 -0
- package/src/code/skills/render.js +15 -1
- package/src/code/taskDecomposer.js +56 -2
- package/src/code/tools/artifactRead.js +40 -0
- package/src/code/tools/askUser.js +11 -0
- package/src/code/tools/planGraph.js +29 -0
- package/src/code/tui.js +2 -0
- package/src/code/usageStore.js +15 -0
- package/src/ui/format/index.js +285 -45
- package/src/ui/format/markdownRenderer.js +436 -71
- package/src/ui/ink/ChatApp.js +39 -8
- package/src/ui/ink/UcodeApp.js +592 -43
- package/src/ui/ink/chatLogModel.js +102 -21
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Plan UI projection — user-facing progress view over planGraph + TaskRuns.
|
|
5
|
+
* Hides IR fields (dependsOn, childGraphId, revisions) from the default surface.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { projectPlanView } = require("./planGraphService");
|
|
9
|
+
const { listActiveWritingTaskRuns } = require("../runtime/taskRun");
|
|
10
|
+
const { hasActiveWriteLease } = require("../runtime/workspaceLease");
|
|
11
|
+
|
|
12
|
+
const ACTIVE_STATUSES = new Set(["running", "waiting_llm", "waiting_approval"]);
|
|
13
|
+
const DONE_STATUSES = new Set(["succeeded"]);
|
|
14
|
+
const FAILED_STATUSES = new Set(["failed", "blocked"]);
|
|
15
|
+
const CANCELLED_STATUSES = new Set(["cancelled", "skipped"]);
|
|
16
|
+
|
|
17
|
+
function ensurePlanUiState(executionState = null) {
|
|
18
|
+
const state = executionState && typeof executionState === "object" ? executionState : {};
|
|
19
|
+
if (!state.planUi || typeof state.planUi !== "object") {
|
|
20
|
+
state.planUi = { bandMode: "auto" };
|
|
21
|
+
}
|
|
22
|
+
if (!state.planUi.bandMode) state.planUi.bandMode = "auto";
|
|
23
|
+
return state.planUi;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getBandMode(executionState = null) {
|
|
27
|
+
return ensurePlanUiState(executionState).bandMode || "auto";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function setBandMode(executionState = null, mode = "auto") {
|
|
31
|
+
const ui = ensurePlanUiState(executionState);
|
|
32
|
+
const next = String(mode || "auto").trim().toLowerCase();
|
|
33
|
+
const allowed = new Set(["auto", "hidden", "expanded", "debug"]);
|
|
34
|
+
ui.bandMode = allowed.has(next) ? next : "auto";
|
|
35
|
+
return ui.bandMode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function statusToMark(status = "") {
|
|
39
|
+
const value = String(status || "").trim().toLowerCase();
|
|
40
|
+
if (DONE_STATUSES.has(value)) return { mark: "✓", kind: "done" };
|
|
41
|
+
if (FAILED_STATUSES.has(value)) return { mark: "✗", kind: "failed" };
|
|
42
|
+
if (CANCELLED_STATUSES.has(value)) return { mark: "⊘", kind: "cancelled" };
|
|
43
|
+
if (ACTIVE_STATUSES.has(value)) return { mark: "→", kind: "active" };
|
|
44
|
+
return { mark: "○", kind: "pending" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function nodeTitle(node = null) {
|
|
48
|
+
if (!node) return "";
|
|
49
|
+
if (node.type === "tool") {
|
|
50
|
+
return node.tool ? `${node.id}:${node.tool}` : (node.title || node.id);
|
|
51
|
+
}
|
|
52
|
+
return String(node.title || node.objective || node.id || "").trim();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function executionKind(node = null) {
|
|
56
|
+
if (!node || node.type !== "task") return "";
|
|
57
|
+
const exec = node.execution;
|
|
58
|
+
if (exec && typeof exec === "object") {
|
|
59
|
+
return String(exec.kind || "").trim().toLowerCase();
|
|
60
|
+
}
|
|
61
|
+
return String(exec || "").trim().toLowerCase();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function truncate(text = "", max = 48) {
|
|
65
|
+
const value = String(text || "").replace(/\s+/g, " ").trim();
|
|
66
|
+
if (!value) return "";
|
|
67
|
+
const limit = Number.isFinite(max) && max > 0 ? Math.floor(max) : 48;
|
|
68
|
+
if (value.length <= limit) return value;
|
|
69
|
+
return `${value.slice(0, Math.max(1, limit - 1))}…`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function basenamePath(filePath = "") {
|
|
73
|
+
const raw = String(filePath || "").trim();
|
|
74
|
+
if (!raw) return "";
|
|
75
|
+
const parts = raw.split(/[/\\]/).filter(Boolean);
|
|
76
|
+
return parts[parts.length - 1] || raw;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function countTaskProgress(view = []) {
|
|
80
|
+
const tasks = view.filter((node) => node && node.type === "task" && !node.generated);
|
|
81
|
+
const total = tasks.length;
|
|
82
|
+
const done = tasks.filter((node) => DONE_STATUSES.has(String(node.status || "").toLowerCase())).length;
|
|
83
|
+
return { done, total };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function resolveFocus(view = [], planGraph = {}, activeRuns = []) {
|
|
87
|
+
const byId = new Map(view.map((node) => [node.id, node]));
|
|
88
|
+
|
|
89
|
+
for (const run of activeRuns) {
|
|
90
|
+
const parent = byId.get(run.parentNodeId);
|
|
91
|
+
if (parent) {
|
|
92
|
+
return {
|
|
93
|
+
nodeId: parent.id,
|
|
94
|
+
title: nodeTitle(parent),
|
|
95
|
+
kind: executionKind(parent) === "task_loop" ? "task_loop" : "task",
|
|
96
|
+
status: parent.status || "running",
|
|
97
|
+
taskRunId: run.id,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const waiting = planGraph.waitingFor;
|
|
103
|
+
if (waiting && waiting.id && byId.has(waiting.id)) {
|
|
104
|
+
const node = byId.get(waiting.id);
|
|
105
|
+
return {
|
|
106
|
+
nodeId: node.id,
|
|
107
|
+
title: nodeTitle(node),
|
|
108
|
+
kind: node.type === "tool" ? "tool" : (executionKind(node) === "task_loop" ? "task_loop" : "task"),
|
|
109
|
+
status: node.status || "waiting_llm",
|
|
110
|
+
taskRunId: "",
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const active = view.find((node) => ACTIVE_STATUSES.has(String(node.status || "").toLowerCase()));
|
|
115
|
+
if (active) {
|
|
116
|
+
return {
|
|
117
|
+
nodeId: active.id,
|
|
118
|
+
title: nodeTitle(active),
|
|
119
|
+
kind: active.type === "tool" ? "tool" : (executionKind(active) === "task_loop" ? "task_loop" : "task"),
|
|
120
|
+
status: active.status,
|
|
121
|
+
taskRunId: "",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const ready = view.find((node) => String(node.status || "").toLowerCase() === "ready" && node.type === "task");
|
|
126
|
+
if (ready) {
|
|
127
|
+
return {
|
|
128
|
+
nodeId: ready.id,
|
|
129
|
+
title: nodeTitle(ready),
|
|
130
|
+
kind: executionKind(ready) === "task_loop" ? "task_loop" : "task",
|
|
131
|
+
status: ready.status,
|
|
132
|
+
taskRunId: "",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildTreeRows(view = [], {
|
|
140
|
+
focusId = "",
|
|
141
|
+
includeToolsUnderFocus = false,
|
|
142
|
+
debug = false,
|
|
143
|
+
} = {}) {
|
|
144
|
+
const byId = new Map(view.map((node) => [node.id, node]));
|
|
145
|
+
const roots = view.filter((node) => (
|
|
146
|
+
node
|
|
147
|
+
&& !node.parentId
|
|
148
|
+
&& node.type === "task"
|
|
149
|
+
&& !node.generated
|
|
150
|
+
));
|
|
151
|
+
const rows = [];
|
|
152
|
+
|
|
153
|
+
function walk(node, depth) {
|
|
154
|
+
if (!node) return;
|
|
155
|
+
const isTool = node.type === "tool";
|
|
156
|
+
if (isTool && !debug) {
|
|
157
|
+
if (!includeToolsUnderFocus || node.parentId !== focusId) return;
|
|
158
|
+
}
|
|
159
|
+
if (!debug && node.generated && node.type !== "task" && node.type !== "tool") return;
|
|
160
|
+
if (!debug && node.type !== "task" && node.type !== "tool") return;
|
|
161
|
+
|
|
162
|
+
const { mark, kind } = statusToMark(node.status);
|
|
163
|
+
rows.push({
|
|
164
|
+
depth,
|
|
165
|
+
id: node.id,
|
|
166
|
+
title: nodeTitle(node),
|
|
167
|
+
mark,
|
|
168
|
+
kind,
|
|
169
|
+
type: node.type,
|
|
170
|
+
status: node.status || "pending",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
const childIds = Array.isArray(node.children) ? node.children : [];
|
|
174
|
+
for (const childId of childIds) {
|
|
175
|
+
const child = byId.get(childId);
|
|
176
|
+
if (!child) continue;
|
|
177
|
+
if (child.type === "task") {
|
|
178
|
+
walk(child, depth + 1);
|
|
179
|
+
} else if (
|
|
180
|
+
child.type === "tool"
|
|
181
|
+
&& (debug || (includeToolsUnderFocus && node.id === focusId))
|
|
182
|
+
) {
|
|
183
|
+
walk(child, depth + 1);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const root of roots) walk(root, 0);
|
|
189
|
+
return rows;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function formatTreeLine(row = {}) {
|
|
193
|
+
const indent = row.depth > 0
|
|
194
|
+
? `${" ".repeat(Math.max(0, row.depth - 1))}├─ `
|
|
195
|
+
: "";
|
|
196
|
+
return `${indent}${row.mark} ${row.title}`;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function buildCompactSummary(rows = [], focusId = "") {
|
|
200
|
+
const top = rows.filter((row) => row.depth === 0 && row.type === "task");
|
|
201
|
+
if (top.length === 0) return "";
|
|
202
|
+
return top
|
|
203
|
+
.map((row) => `${row.title} ${row.mark}`)
|
|
204
|
+
.join(" · ");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function buildDebugLines(executionState = null, planGraph = {}) {
|
|
208
|
+
const lines = [];
|
|
209
|
+
const pg = planGraph && typeof planGraph === "object" ? planGraph : {};
|
|
210
|
+
lines.push(`graphId=${pg.graphId || "-"} spec=${Number(pg.specRevision) || 0} state=${Number(pg.stateRevision) || 0}`);
|
|
211
|
+
const nodes = Array.isArray(pg.nodes) ? pg.nodes : [];
|
|
212
|
+
for (const node of nodes.slice(0, 12)) {
|
|
213
|
+
const deps = Array.isArray(node.dependsOn) ? node.dependsOn.join(",") : "";
|
|
214
|
+
lines.push(
|
|
215
|
+
`${node.id} type=${node.type} status=${node.status || "pending"}`
|
|
216
|
+
+ (deps ? ` deps=[${deps}]` : "")
|
|
217
|
+
+ (node.parentTaskId ? ` parent=${node.parentTaskId}` : "")
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (nodes.length > 12) lines.push(`… +${nodes.length - 12} nodes`);
|
|
221
|
+
const runs = listActiveWritingTaskRuns(executionState);
|
|
222
|
+
for (const run of runs) {
|
|
223
|
+
lines.push(`taskRun ${run.id} node=${run.parentNodeId} status=${run.status} phase=${run.phase || ""}`);
|
|
224
|
+
}
|
|
225
|
+
const lease = executionState && executionState.workspaceLease;
|
|
226
|
+
if (lease && lease.holder) {
|
|
227
|
+
lines.push(`lease ${lease.holder.kind}${lease.holder.taskRunId ? `:${lease.holder.taskRunId}` : ""}`);
|
|
228
|
+
}
|
|
229
|
+
return lines;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildTaskRunProjection(executionState = null, view = []) {
|
|
233
|
+
const active = listActiveWritingTaskRuns(executionState)[0] || null;
|
|
234
|
+
if (!active) return null;
|
|
235
|
+
const byId = new Map(view.map((node) => [node.id, node]));
|
|
236
|
+
const parent = byId.get(active.parentNodeId);
|
|
237
|
+
const files = Array.isArray(active.changedFiles) ? active.changedFiles : [];
|
|
238
|
+
const hint = files.slice(-2).map(basenamePath).filter(Boolean).join(", ");
|
|
239
|
+
return {
|
|
240
|
+
phase: String(active.phase || active.status || "running"),
|
|
241
|
+
status: String(active.status || ""),
|
|
242
|
+
parentTitle: parent ? nodeTitle(parent) : active.parentNodeId,
|
|
243
|
+
taskRunId: active.id,
|
|
244
|
+
changedFilesHint: hint,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function projectionHash({
|
|
249
|
+
bandMode = "auto",
|
|
250
|
+
specRevision = 0,
|
|
251
|
+
stateRevision = 0,
|
|
252
|
+
focusId = "",
|
|
253
|
+
taskRunId = "",
|
|
254
|
+
leaseHeld = false,
|
|
255
|
+
progressDone = 0,
|
|
256
|
+
progressTotal = 0,
|
|
257
|
+
bandLines = [],
|
|
258
|
+
} = {}) {
|
|
259
|
+
return [
|
|
260
|
+
bandMode,
|
|
261
|
+
specRevision,
|
|
262
|
+
stateRevision,
|
|
263
|
+
focusId,
|
|
264
|
+
taskRunId,
|
|
265
|
+
leaseHeld ? "1" : "0",
|
|
266
|
+
progressDone,
|
|
267
|
+
progressTotal,
|
|
268
|
+
bandLines.join("\n"),
|
|
269
|
+
].join("|");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Build TUI-facing plan projection.
|
|
274
|
+
*
|
|
275
|
+
* @param {object|null} executionState
|
|
276
|
+
* @param {{ cols?: number, activityMessage?: string, maxBandRows?: number }} [options]
|
|
277
|
+
*/
|
|
278
|
+
function buildPlanUiProjection(executionState = null, options = {}) {
|
|
279
|
+
const state = executionState && typeof executionState === "object" ? executionState : {};
|
|
280
|
+
const bandMode = getBandMode(state);
|
|
281
|
+
const cols = Number(options.cols) > 0 ? Math.floor(Number(options.cols)) : 80;
|
|
282
|
+
const narrow = cols < 60;
|
|
283
|
+
const activityMessage = String(options.activityMessage || "").trim();
|
|
284
|
+
|
|
285
|
+
const pg = state.planGraph && typeof state.planGraph === "object" ? state.planGraph : {};
|
|
286
|
+
const view = projectPlanView(pg);
|
|
287
|
+
const taskNodes = view.filter((node) => node && node.type === "task" && !node.generated);
|
|
288
|
+
const hasPlan = Boolean(pg.graphId) && taskNodes.length > 0;
|
|
289
|
+
const progress = countTaskProgress(view);
|
|
290
|
+
const activeRuns = listActiveWritingTaskRuns(state);
|
|
291
|
+
const focus = resolveFocus(view, pg, activeRuns);
|
|
292
|
+
const taskRun = buildTaskRunProjection(state, view);
|
|
293
|
+
const leaseHeld = hasActiveWriteLease(state);
|
|
294
|
+
|
|
295
|
+
const includeTools = bandMode === "expanded" || bandMode === "debug";
|
|
296
|
+
const tree = hasPlan
|
|
297
|
+
? buildTreeRows(view, {
|
|
298
|
+
focusId: focus ? focus.nodeId : "",
|
|
299
|
+
includeToolsUnderFocus: includeTools,
|
|
300
|
+
debug: bandMode === "debug",
|
|
301
|
+
})
|
|
302
|
+
: [];
|
|
303
|
+
|
|
304
|
+
const progressLabel = progress.total > 0 ? `${progress.done}/${progress.total}` : "";
|
|
305
|
+
const focusTitle = focus ? truncate(focus.title, narrow ? 18 : 28) : "";
|
|
306
|
+
|
|
307
|
+
let bandLines = [];
|
|
308
|
+
let visible = false;
|
|
309
|
+
|
|
310
|
+
if (hasPlan && bandMode !== "hidden") {
|
|
311
|
+
visible = true;
|
|
312
|
+
if (bandMode === "debug") {
|
|
313
|
+
bandLines = buildDebugLines(state, pg);
|
|
314
|
+
} else if (narrow) {
|
|
315
|
+
const summary = buildCompactSummary(tree, focus && focus.nodeId);
|
|
316
|
+
bandLines = [truncate(
|
|
317
|
+
`Plan${focusTitle ? ` · ${focusTitle}` : ""}${progressLabel ? ` (${progressLabel})` : ""}${summary && !focusTitle ? ` ${summary}` : ""}`,
|
|
318
|
+
Math.max(24, cols - 2)
|
|
319
|
+
)];
|
|
320
|
+
} else if (bandMode === "auto") {
|
|
321
|
+
const summary = buildCompactSummary(tree, focus && focus.nodeId);
|
|
322
|
+
const header = truncate(
|
|
323
|
+
`Plan${pg.objective ? ` · ${pg.objective}` : ""}${summary ? ` ${summary}` : ""}`,
|
|
324
|
+
Math.max(24, cols - 2)
|
|
325
|
+
);
|
|
326
|
+
bandLines = [header];
|
|
327
|
+
if (focus) {
|
|
328
|
+
const focusChildren = view
|
|
329
|
+
.filter((node) => node && node.parentId === focus.nodeId)
|
|
330
|
+
.map((node) => {
|
|
331
|
+
const { mark } = statusToMark(node.status);
|
|
332
|
+
return `${mark} ${nodeTitle(node)}`;
|
|
333
|
+
});
|
|
334
|
+
if (focusChildren.length > 0) {
|
|
335
|
+
bandLines.push(truncate(
|
|
336
|
+
` └ ${focusChildren.join(" · ")}`,
|
|
337
|
+
Math.max(24, cols - 2)
|
|
338
|
+
));
|
|
339
|
+
} else if (focus.title) {
|
|
340
|
+
bandLines.push(truncate(` → ${focus.title}`, Math.max(24, cols - 2)));
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (taskRun) {
|
|
344
|
+
const leaseBit = leaseHeld ? "writing" : taskRun.phase;
|
|
345
|
+
const files = taskRun.changedFilesHint ? ` · ${taskRun.changedFilesHint}` : "";
|
|
346
|
+
bandLines.push(truncate(` TaskLoop ${leaseBit}${files}`, Math.max(24, cols - 2)));
|
|
347
|
+
}
|
|
348
|
+
const maxRows = Number.isFinite(options.maxBandRows) ? options.maxBandRows : 3;
|
|
349
|
+
bandLines = bandLines.slice(0, Math.max(1, maxRows));
|
|
350
|
+
} else {
|
|
351
|
+
// expanded
|
|
352
|
+
const title = pg.objective ? `Plan · ${pg.objective}` : "Plan";
|
|
353
|
+
bandLines = [truncate(title, Math.max(24, cols - 2))];
|
|
354
|
+
for (const row of tree) {
|
|
355
|
+
bandLines.push(truncate(formatTreeLine(row), Math.max(24, cols - 2)));
|
|
356
|
+
}
|
|
357
|
+
if (taskRun) {
|
|
358
|
+
const files = taskRun.changedFilesHint ? ` · ${taskRun.changedFilesHint}` : "";
|
|
359
|
+
bandLines.push(truncate(`TaskLoop · ${taskRun.phase}${files}`, Math.max(24, cols - 2)));
|
|
360
|
+
}
|
|
361
|
+
const maxRows = Number.isFinite(options.maxBandRows) ? options.maxBandRows : 7;
|
|
362
|
+
bandLines = bandLines.slice(0, Math.max(1, maxRows));
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const progressLabelForStatus = progress.total > 0 ? `${progress.done}/${progress.total}` : "";
|
|
367
|
+
let statusLine = "";
|
|
368
|
+
if (hasPlan) {
|
|
369
|
+
const parts = ["Plan"];
|
|
370
|
+
if (focusTitle) parts.push(focusTitle);
|
|
371
|
+
if (progressLabelForStatus) parts.push(`(${progressLabelForStatus})`);
|
|
372
|
+
if (taskRun) parts.push(`TaskLoop ${taskRun.phase || "running"}`);
|
|
373
|
+
else if (focus && ACTIVE_STATUSES.has(String(focus.status || "").toLowerCase())) {
|
|
374
|
+
parts.push(String(focus.status).replace(/_/g, " "));
|
|
375
|
+
}
|
|
376
|
+
statusLine = parts.join(" · ");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
let idleHint = "";
|
|
380
|
+
if (hasPlan && progress.total > 0 && progress.done < progress.total) {
|
|
381
|
+
idleHint = focusTitle
|
|
382
|
+
? `Plan waiting: ${focusTitle}${progressLabelForStatus ? ` (${progressLabelForStatus})` : ""}`
|
|
383
|
+
: `Plan (${progressLabelForStatus})`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let activityStatusLine = activityMessage;
|
|
387
|
+
if (hasPlan && statusLine) {
|
|
388
|
+
if (!activityMessage) {
|
|
389
|
+
activityStatusLine = statusLine;
|
|
390
|
+
} else {
|
|
391
|
+
const tail = truncate(activityMessage, narrow ? 24 : 36);
|
|
392
|
+
activityStatusLine = `${statusLine} · ${tail}`;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const hash = projectionHash({
|
|
397
|
+
bandMode,
|
|
398
|
+
specRevision: Number(pg.specRevision) || 0,
|
|
399
|
+
stateRevision: Number(pg.stateRevision) || 0,
|
|
400
|
+
focusId: focus ? focus.nodeId : "",
|
|
401
|
+
taskRunId: taskRun ? taskRun.taskRunId : "",
|
|
402
|
+
leaseHeld,
|
|
403
|
+
progressDone: progress.done,
|
|
404
|
+
progressTotal: progress.total,
|
|
405
|
+
bandLines,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
hasPlan,
|
|
410
|
+
visible,
|
|
411
|
+
bandMode,
|
|
412
|
+
objective: String(pg.objective || "").trim(),
|
|
413
|
+
progress,
|
|
414
|
+
focus,
|
|
415
|
+
tree,
|
|
416
|
+
taskRun,
|
|
417
|
+
leaseHeld,
|
|
418
|
+
statusLine,
|
|
419
|
+
idleHint,
|
|
420
|
+
activityStatusLine,
|
|
421
|
+
bandLines,
|
|
422
|
+
hash,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
module.exports = {
|
|
427
|
+
ensurePlanUiState,
|
|
428
|
+
getBandMode,
|
|
429
|
+
setBandMode,
|
|
430
|
+
statusToMark,
|
|
431
|
+
buildPlanUiProjection,
|
|
432
|
+
};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const { runToolCall } = require("../dispatch");
|
|
6
|
+
const { saveArtifact, createArtifactId, hashContent } = require("./artifacts");
|
|
7
|
+
|
|
8
|
+
const PREFLIGHT_FILES = [
|
|
9
|
+
"AGENTS.md",
|
|
10
|
+
"README.md",
|
|
11
|
+
"README.zh-CN.md",
|
|
12
|
+
"package.json",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
function readFileIfExists(workspaceRoot = process.cwd(), relPath = "") {
|
|
16
|
+
const full = path.resolve(workspaceRoot, relPath);
|
|
17
|
+
try {
|
|
18
|
+
if (!fs.existsSync(full) || !fs.statSync(full).isFile()) return null;
|
|
19
|
+
const content = fs.readFileSync(full, "utf8");
|
|
20
|
+
return { path: relPath, content, hash: hashContent(content) };
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function summarizePackageJson(content = "") {
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(content);
|
|
29
|
+
return {
|
|
30
|
+
name: parsed.name || "",
|
|
31
|
+
packageManager: parsed.packageManager || "",
|
|
32
|
+
scripts: Object.keys(parsed.scripts || {}).slice(0, 8),
|
|
33
|
+
};
|
|
34
|
+
} catch {
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function summarizeAgentsRules(content = "") {
|
|
40
|
+
const lines = String(content || "").split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
41
|
+
return lines.slice(0, 12);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function summarizeReadme(content = "") {
|
|
45
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
46
|
+
const headings = lines
|
|
47
|
+
.filter((l) => /^#{1,3}\s+/.test(l))
|
|
48
|
+
.slice(0, 10);
|
|
49
|
+
const intro = lines.filter((l) => l.trim() && !l.startsWith("#")).slice(0, 3).join(" ");
|
|
50
|
+
return { headings, intro: intro.slice(0, 240) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function collectCurrentFileHashes(workspaceRoot = process.cwd()) {
|
|
54
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
55
|
+
return PREFLIGHT_FILES.map((relPath) => {
|
|
56
|
+
const file = readFileIfExists(root, relPath);
|
|
57
|
+
return file ? { path: relPath, hash: file.hash } : null;
|
|
58
|
+
}).filter(Boolean);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isProjectSnapshotStale(snapshot = null, workspaceRoot = process.cwd()) {
|
|
62
|
+
if (!snapshot || !snapshot.projectSnapshotId || !Array.isArray(snapshot.files)) return true;
|
|
63
|
+
const current = collectCurrentFileHashes(workspaceRoot);
|
|
64
|
+
if (current.length !== snapshot.files.length) return true;
|
|
65
|
+
const byPath = new Map(snapshot.files.map((entry) => [entry.path, entry.hash]));
|
|
66
|
+
for (const entry of current) {
|
|
67
|
+
if (byPath.get(entry.path) !== entry.hash) return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function invalidateProjectSnapshotIfPathTouched(session = {}, filePath = "") {
|
|
73
|
+
if (!session || typeof session !== "object") return false;
|
|
74
|
+
const rel = String(filePath || "").trim().replace(/\\/g, "/");
|
|
75
|
+
if (!rel) return false;
|
|
76
|
+
const touched = PREFLIGHT_FILES.some((name) => (
|
|
77
|
+
rel === name || rel.endsWith(`/${name}`)
|
|
78
|
+
));
|
|
79
|
+
if (!touched) return false;
|
|
80
|
+
session.projectSnapshot = null;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildProjectSnapshot({
|
|
85
|
+
workspaceRoot = process.cwd(),
|
|
86
|
+
sessionId = "",
|
|
87
|
+
existing = null,
|
|
88
|
+
} = {}) {
|
|
89
|
+
const root = path.resolve(workspaceRoot || process.cwd());
|
|
90
|
+
if (existing && !isProjectSnapshotStale(existing, root)) {
|
|
91
|
+
return existing;
|
|
92
|
+
}
|
|
93
|
+
const files = [];
|
|
94
|
+
const summary = {
|
|
95
|
+
language: "",
|
|
96
|
+
packageManager: "",
|
|
97
|
+
entryPoints: [],
|
|
98
|
+
rules: [],
|
|
99
|
+
readmeHeadings: [],
|
|
100
|
+
readmeIntro: "",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
for (const relPath of PREFLIGHT_FILES) {
|
|
104
|
+
const file = readFileIfExists(root, relPath);
|
|
105
|
+
if (!file) continue;
|
|
106
|
+
const artifactId = createArtifactId(`artifact_${relPath.replace(/[^a-zA-Z0-9]+/g, "_")}`);
|
|
107
|
+
saveArtifact(root, sessionId, {
|
|
108
|
+
artifactId,
|
|
109
|
+
type: "source_file",
|
|
110
|
+
source: relPath,
|
|
111
|
+
tool: "read",
|
|
112
|
+
raw: { ok: true, path: relPath, content: file.content },
|
|
113
|
+
summary: relPath,
|
|
114
|
+
createdBy: "project_snapshot",
|
|
115
|
+
});
|
|
116
|
+
files.push({
|
|
117
|
+
path: relPath,
|
|
118
|
+
artifactId,
|
|
119
|
+
hash: file.hash,
|
|
120
|
+
});
|
|
121
|
+
if (relPath === "package.json") {
|
|
122
|
+
const pkg = summarizePackageJson(file.content);
|
|
123
|
+
summary.packageManager = pkg.packageManager || "";
|
|
124
|
+
summary.language = "Node.js";
|
|
125
|
+
summary.entryPoints = pkg.scripts || [];
|
|
126
|
+
}
|
|
127
|
+
if (relPath === "AGENTS.md") {
|
|
128
|
+
summary.rules = summarizeAgentsRules(file.content);
|
|
129
|
+
}
|
|
130
|
+
if (relPath.startsWith("README")) {
|
|
131
|
+
const readme = summarizeReadme(file.content);
|
|
132
|
+
summary.readmeHeadings = readme.headings;
|
|
133
|
+
summary.readmeIntro = readme.intro;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const snapshotId = `project_snapshot_${hashContent(JSON.stringify(files))}`;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
projectSnapshotId: snapshotId,
|
|
141
|
+
files,
|
|
142
|
+
summary,
|
|
143
|
+
createdAt: new Date().toISOString(),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function renderProjectSnapshotContext(snapshot = null) {
|
|
148
|
+
if (!snapshot || !snapshot.projectSnapshotId) return "";
|
|
149
|
+
const summary = snapshot.summary && typeof snapshot.summary === "object" ? snapshot.summary : {};
|
|
150
|
+
const fileRefs = (Array.isArray(snapshot.files) ? snapshot.files : [])
|
|
151
|
+
.map((f) => `- ${f.path}: artifact://${f.artifactId} (hash ${f.hash})`)
|
|
152
|
+
.join("\n");
|
|
153
|
+
const lines = [
|
|
154
|
+
"Project Snapshot:",
|
|
155
|
+
summary.language ? `- Language: ${summary.language}` : "",
|
|
156
|
+
summary.packageManager ? `- Package manager: ${summary.packageManager}` : "",
|
|
157
|
+
summary.rules && summary.rules.length > 0
|
|
158
|
+
? `- Repository rules: ${summary.rules.slice(0, 5).join("; ")}`
|
|
159
|
+
: "",
|
|
160
|
+
summary.readmeIntro ? `- README intro: ${summary.readmeIntro}` : "",
|
|
161
|
+
fileRefs ? `Files:\n${fileRefs}` : "",
|
|
162
|
+
].filter(Boolean);
|
|
163
|
+
return lines.join("\n");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function createProjectPreflightContextV2({
|
|
167
|
+
workspaceRoot = process.cwd(),
|
|
168
|
+
sessionId = "",
|
|
169
|
+
pushToolLog = () => null,
|
|
170
|
+
existingSnapshot = null,
|
|
171
|
+
} = {}) {
|
|
172
|
+
const root = String(workspaceRoot || process.cwd());
|
|
173
|
+
for (const relPath of PREFLIGHT_FILES) {
|
|
174
|
+
pushToolLog({ tool: "read", phase: "start", args: { path: relPath }, error: "" });
|
|
175
|
+
const readRes = runToolCall(
|
|
176
|
+
{ tool: "read", args: { path: relPath, maxBytes: 12000 } },
|
|
177
|
+
{ workspaceRoot: root, cwd: root },
|
|
178
|
+
);
|
|
179
|
+
pushToolLog({
|
|
180
|
+
tool: "read",
|
|
181
|
+
phase: readRes && readRes.ok === false ? "error" : "",
|
|
182
|
+
args: { path: relPath },
|
|
183
|
+
error: readRes && readRes.ok === false ? String(readRes.error || "") : "",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return buildProjectSnapshot({
|
|
187
|
+
workspaceRoot: root,
|
|
188
|
+
sessionId,
|
|
189
|
+
existing: existingSnapshot,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = {
|
|
194
|
+
PREFLIGHT_FILES,
|
|
195
|
+
buildProjectSnapshot,
|
|
196
|
+
renderProjectSnapshotContext,
|
|
197
|
+
createProjectPreflightContextV2,
|
|
198
|
+
isProjectSnapshotStale,
|
|
199
|
+
invalidateProjectSnapshotIfPathTouched,
|
|
200
|
+
collectCurrentFileHashes,
|
|
201
|
+
};
|