@bastani/atomic 0.9.0-alpha.3 → 0.9.0-alpha.4
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/CHANGELOG.md +15 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +17 -0
- package/dist/builtin/workflows/README.md +12 -12
- package/dist/builtin/workflows/builtin/goal-prompts.ts +8 -0
- package/dist/builtin/workflows/builtin/goal-runner.ts +96 -1
- package/dist/builtin/workflows/builtin/goal-types.ts +2 -0
- package/dist/builtin/workflows/builtin/goal.d.ts +3 -0
- package/dist/builtin/workflows/builtin/goal.ts +12 -1
- package/dist/builtin/workflows/builtin/index.d.ts +8 -8
- package/dist/builtin/workflows/builtin/open-claude-design-feedback.ts +359 -0
- package/dist/builtin/workflows/builtin/open-claude-design-phases.ts +254 -352
- package/dist/builtin/workflows/builtin/open-claude-design-runner.ts +256 -414
- package/dist/builtin/workflows/builtin/open-claude-design-setup.ts +272 -0
- package/dist/builtin/workflows/builtin/open-claude-design-utils.ts +58 -68
- package/dist/builtin/workflows/builtin/open-claude-design.d.ts +5 -9
- package/dist/builtin/workflows/builtin/open-claude-design.ts +14 -26
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/skills/impeccable/SKILL.md +14 -23
- package/dist/builtin/workflows/skills/impeccable/reference/brand.md +2 -2
- package/dist/builtin/workflows/skills/impeccable/reference/live.md +25 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/context-signals.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/context.mjs +724 -29
- package/dist/builtin/workflows/skills/impeccable/scripts/critique-storage.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/browser/injected/index.mjs +219 -7
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/cli/main.mjs +57 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/design-system.mjs +750 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns-browser.js +648 -53
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/detect-antipatterns.mjs +7 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/browser/detect-url.mjs +29 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/regex/detect-text.mjs +44 -11
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +29 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/engines/static-html/detect-html.mjs +27 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/node/file-system.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/registry/antipatterns.mjs +29 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/rules/checks.mjs +401 -46
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/detector/shared/page.mjs +6 -6
- package/dist/builtin/workflows/skills/impeccable/scripts/{design-parser.mjs → lib/design-parser.mjs} +8 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-config.mjs +638 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/impeccable-paths.mjs +128 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{is-generated.mjs → lib/is-generated.mjs} +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/lib/target-args.mjs +42 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/browser-script-parts.mjs +49 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-completion.mjs → live/completion.mjs} +1 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-event-validation.mjs → live/event-validation.mjs} +6 -5
- package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-apply.mjs +939 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-manual-edits-buffer.mjs → live/manual-edits-buffer.mjs} +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/{live-session-store.mjs → live/session-store.mjs} +21 -3
- package/dist/builtin/workflows/skills/impeccable/scripts/live/svelte-component.mjs +835 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/sveltekit-adapter.mjs +274 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/ui-core.mjs +180 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live/vocabulary.mjs +36 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-accept.mjs +185 -60
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser-dom.js +146 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-browser.js +3369 -1026
- package/dist/builtin/workflows/skills/impeccable/scripts/live-commit-manual-edits.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-complete.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-discard-manual-edits.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-inject.mjs +133 -9
- package/dist/builtin/workflows/skills/impeccable/scripts/live-insert.mjs +42 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-manual-edit-evidence.mjs +4 -4
- package/dist/builtin/workflows/skills/impeccable/scripts/live-poll.mjs +21 -15
- package/dist/builtin/workflows/skills/impeccable/scripts/live-resume.mjs +1 -1
- package/dist/builtin/workflows/skills/impeccable/scripts/live-server.mjs +205 -1269
- package/dist/builtin/workflows/skills/impeccable/scripts/live-status.mjs +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/live-target.mjs +30 -0
- package/dist/builtin/workflows/skills/impeccable/scripts/live-wrap.mjs +69 -26
- package/dist/builtin/workflows/skills/impeccable/scripts/live.mjs +73 -22
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +5 -5
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/docs/index.md +2 -2
- package/docs/quickstart.md +9 -9
- package/docs/workflows.md +42 -23
- package/package.json +2 -2
- package/dist/builtin/workflows/skills/impeccable/scripts/cleanup-deprecated.mjs +0 -284
- package/dist/builtin/workflows/skills/impeccable/scripts/impeccable-paths.mjs +0 -126
- /package/dist/builtin/workflows/skills/impeccable/scripts/{live-insert-ui.mjs → live/insert-ui.mjs} +0 -0
|
@@ -20,10 +20,18 @@ import fs from 'node:fs';
|
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
import net from 'node:net';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
|
-
import { parseDesignMd } from './design-parser.mjs';
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
23
|
+
import { parseDesignMd } from './lib/design-parser.mjs';
|
|
24
|
+
import { loadContext } from './context.mjs';
|
|
25
|
+
import {
|
|
26
|
+
assembleLiveBrowserScript,
|
|
27
|
+
assertLiveBrowserScriptParts,
|
|
28
|
+
readLiveBrowserScriptParts,
|
|
29
|
+
resolveLiveBrowserScriptParts,
|
|
30
|
+
} from './live/browser-script-parts.mjs';
|
|
31
|
+
import { createLiveSessionStore } from './live/session-store.mjs';
|
|
32
|
+
import { validateEvent } from './live/event-validation.mjs';
|
|
33
|
+
import { createManualEditRoutes } from './live/manual-edit-routes.mjs';
|
|
34
|
+
import { LIVE_COMMANDS } from './live/vocabulary.mjs';
|
|
27
35
|
import {
|
|
28
36
|
getDesignSidecarPath,
|
|
29
37
|
getLiveDir,
|
|
@@ -32,22 +40,26 @@ import {
|
|
|
32
40
|
removeLiveServerInfo,
|
|
33
41
|
resolveDesignSidecarPath,
|
|
34
42
|
writeLiveServerInfo,
|
|
35
|
-
} from './impeccable-paths.mjs';
|
|
43
|
+
} from './lib/impeccable-paths.mjs';
|
|
44
|
+
import { countByPage as countPendingByPage } from './live/manual-edits-buffer.mjs';
|
|
45
|
+
import {
|
|
46
|
+
createManualApplyController,
|
|
47
|
+
summarizeManualApplyFailures,
|
|
48
|
+
} from './live/manual-apply.mjs';
|
|
36
49
|
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
stageEntry as stageManualEditEntry,
|
|
41
|
-
truncateBuffer as truncateManualEditsBuffer,
|
|
42
|
-
} from './live-manual-edits-buffer.mjs';
|
|
43
|
-
import { buildManualEditEvidence } from './live-manual-edit-evidence.mjs';
|
|
44
|
-
import { commitManualEdits } from './live-commit-manual-edits.mjs';
|
|
50
|
+
applyDeferredSvelteComponentAccepts,
|
|
51
|
+
removeAllSvelteComponentSessions,
|
|
52
|
+
} from './live/svelte-component.mjs';
|
|
45
53
|
|
|
46
54
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
47
55
|
// PRODUCT.md / DESIGN.md live wherever context.mjs resolves. The generated
|
|
48
56
|
// DESIGN sidecar is project-local at .impeccable/design.json, with legacy
|
|
49
57
|
// DESIGN.json fallback for existing projects.
|
|
50
|
-
const
|
|
58
|
+
const PROJECT_CONTEXT = loadContext(process.cwd());
|
|
59
|
+
const CONTEXT_DIR = PROJECT_CONTEXT.contextDir;
|
|
60
|
+
const DESIGN_MD_PATH = PROJECT_CONTEXT.designPath
|
|
61
|
+
? path.resolve(process.cwd(), PROJECT_CONTEXT.designPath)
|
|
62
|
+
: null;
|
|
51
63
|
const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
|
|
52
64
|
const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
|
|
53
65
|
|
|
@@ -96,22 +108,29 @@ const state = {
|
|
|
96
108
|
};
|
|
97
109
|
|
|
98
110
|
const CHAT_POLL_FRESHNESS_MS = 60_000;
|
|
99
|
-
const
|
|
100
|
-
const APPLY_EVENT_SOFT_DEADLINE_MS = Number(process.env.IMPECCABLE_LIVE_APPLY_EVENT_SOFT_DEADLINE_MS || 120_000);
|
|
101
|
-
const DEFAULT_MANUAL_EDIT_APPLY_CHUNK_SIZE = 3;
|
|
102
|
-
const MIN_MANUAL_EDIT_APPLY_CHUNK_SIZE = 1;
|
|
103
|
-
const MAX_MANUAL_EDIT_APPLY_CHUNK_SIZE = 20;
|
|
104
|
-
const MANUAL_APPLY_COMPACT_TEXT_LIMIT = 240;
|
|
105
|
-
const MANUAL_APPLY_COMPACT_NEARBY_LIMIT = 4;
|
|
111
|
+
const POLL_LEASE_EXPIRY_TIMER_GRACE_MS = 2;
|
|
106
112
|
const DEBUG_MANUAL_EDIT_EVENTS = /^(1|true|yes)$/i.test(process.env.IMPECCABLE_LIVE_DEBUG_EVENTS || '');
|
|
107
113
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
state.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
const manualApply = createManualApplyController({
|
|
115
|
+
pendingEvents: state.pendingEvents,
|
|
116
|
+
pendingApplyDeferreds: state.pendingApplyDeferreds,
|
|
117
|
+
timedOutApplyIds: state.timedOutApplyIds,
|
|
118
|
+
enqueueEvent,
|
|
119
|
+
acknowledgePendingEvent,
|
|
120
|
+
flushPendingPolls,
|
|
121
|
+
recordManualEditActivity,
|
|
122
|
+
cwd: () => process.cwd(),
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const manualEditRoutes = createManualEditRoutes({
|
|
126
|
+
getToken: () => state.token,
|
|
127
|
+
manualApply,
|
|
128
|
+
recordManualEditActivity,
|
|
129
|
+
getManualEditStatus,
|
|
130
|
+
chatAgentLikelyActive,
|
|
131
|
+
cwd: () => process.cwd(),
|
|
132
|
+
env: () => process.env,
|
|
133
|
+
});
|
|
115
134
|
|
|
116
135
|
function chatAgentLikelyActive() {
|
|
117
136
|
if (state.pendingPolls.length > 0) return true;
|
|
@@ -119,752 +138,6 @@ function chatAgentLikelyActive() {
|
|
|
119
138
|
return Date.now() - state.lastPollAt < CHAT_POLL_FRESHNESS_MS;
|
|
120
139
|
}
|
|
121
140
|
|
|
122
|
-
function manualEditApplyChunkSize(env = process.env) {
|
|
123
|
-
const raw = Number(env.IMPECCABLE_LIVE_MANUAL_EDIT_CHUNK_SIZE);
|
|
124
|
-
if (!Number.isFinite(raw)) return DEFAULT_MANUAL_EDIT_APPLY_CHUNK_SIZE;
|
|
125
|
-
const size = Math.trunc(raw);
|
|
126
|
-
return Math.max(MIN_MANUAL_EDIT_APPLY_CHUNK_SIZE, Math.min(MAX_MANUAL_EDIT_APPLY_CHUNK_SIZE, size));
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function countManualApplyOps(entriesOrBatch) {
|
|
130
|
-
const entries = Array.isArray(entriesOrBatch)
|
|
131
|
-
? entriesOrBatch
|
|
132
|
-
: Array.isArray(entriesOrBatch?.entries) ? entriesOrBatch.entries : [];
|
|
133
|
-
let count = 0;
|
|
134
|
-
for (const entry of entries) count += Array.isArray(entry.ops) ? entry.ops.length : 0;
|
|
135
|
-
return count;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function pushApplyEventAndWait(batch, pageUrl, chunk = null, repair = null) {
|
|
139
|
-
const eventId = randomUUID().replace(/-/g, '').slice(0, 8);
|
|
140
|
-
const evidencePath = writeManualApplyEvidence(eventId, batch);
|
|
141
|
-
const event = {
|
|
142
|
-
type: 'manual_edit_apply',
|
|
143
|
-
id: eventId,
|
|
144
|
-
pageUrl,
|
|
145
|
-
batch: compactManualApplyBatch(batch),
|
|
146
|
-
evidencePath,
|
|
147
|
-
agentAction: buildManualApplyAgentAction(eventId),
|
|
148
|
-
schemaVersion: 1,
|
|
149
|
-
deadlineMs: APPLY_EVENT_SOFT_DEADLINE_MS,
|
|
150
|
-
};
|
|
151
|
-
if (chunk) event.chunk = chunk;
|
|
152
|
-
if (repair) event.repair = repair;
|
|
153
|
-
const rollbackSnapshot = snapshotApplyEventFiles(batch);
|
|
154
|
-
recordManualEditActivity('manual_edit_apply_dispatched', {
|
|
155
|
-
id: eventId,
|
|
156
|
-
pageUrl,
|
|
157
|
-
chunk,
|
|
158
|
-
repair,
|
|
159
|
-
entryCount: Array.isArray(batch.entries) ? batch.entries.length : 0,
|
|
160
|
-
opCount: countManualApplyOps(batch),
|
|
161
|
-
fileCount: collectManualApplyFiles(batch).length,
|
|
162
|
-
});
|
|
163
|
-
return new Promise((resolve, reject) => {
|
|
164
|
-
const timer = setTimeout(() => {
|
|
165
|
-
state.pendingApplyDeferreds.delete(eventId);
|
|
166
|
-
tombstoneTimedOutApplyId(eventId, { batch, rollbackSnapshot });
|
|
167
|
-
acknowledgePendingEvent(eventId);
|
|
168
|
-
removeManualApplyEvidence(evidencePath);
|
|
169
|
-
recordManualEditActivity('manual_edit_apply_timeout', {
|
|
170
|
-
id: eventId,
|
|
171
|
-
pageUrl,
|
|
172
|
-
chunk,
|
|
173
|
-
entryCount: Array.isArray(batch.entries) ? batch.entries.length : 0,
|
|
174
|
-
opCount: countManualApplyOps(batch),
|
|
175
|
-
});
|
|
176
|
-
reject(new Error('chat_agent_timeout'));
|
|
177
|
-
}, APPLY_EVENT_HARD_TIMEOUT_MS);
|
|
178
|
-
state.pendingApplyDeferreds.set(eventId, { resolve, reject, timer, event, batch, pageUrl, rollbackSnapshot });
|
|
179
|
-
enqueueEvent(event);
|
|
180
|
-
});
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function writeManualApplyEvidence(eventId, batch) {
|
|
184
|
-
const dir = manualApplyEvidenceDir(process.cwd());
|
|
185
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
186
|
-
const evidencePath = path.join(dir, `${eventId}.json`);
|
|
187
|
-
fs.writeFileSync(evidencePath, JSON.stringify(batch, null, 2) + '\n', 'utf-8');
|
|
188
|
-
return evidencePath;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function manualApplyEvidenceDir(cwd = process.cwd()) {
|
|
192
|
-
return path.join(getLiveDir(cwd), 'manual-edit-evidence');
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function normalizeManualApplyEvidencePath(evidencePath, cwd = process.cwd()) {
|
|
196
|
-
if (!evidencePath || typeof evidencePath !== 'string') return null;
|
|
197
|
-
const fullPath = path.isAbsolute(evidencePath) ? evidencePath : path.resolve(cwd, evidencePath);
|
|
198
|
-
const evidenceDir = manualApplyEvidenceDir(cwd);
|
|
199
|
-
const relative = path.relative(evidenceDir, fullPath);
|
|
200
|
-
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;
|
|
201
|
-
if (path.extname(relative) !== '.json') return null;
|
|
202
|
-
return fullPath;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function removeManualApplyEvidence(evidencePath, cwd = process.cwd()) {
|
|
206
|
-
const fullPath = normalizeManualApplyEvidencePath(evidencePath, cwd);
|
|
207
|
-
if (!fullPath) return false;
|
|
208
|
-
try {
|
|
209
|
-
fs.unlinkSync(fullPath);
|
|
210
|
-
return true;
|
|
211
|
-
} catch {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function referencedManualApplyEvidencePaths(cwd = process.cwd()) {
|
|
217
|
-
const referenced = new Set();
|
|
218
|
-
const add = (event) => {
|
|
219
|
-
const fullPath = normalizeManualApplyEvidencePath(event?.evidencePath, cwd);
|
|
220
|
-
if (fullPath) referenced.add(fullPath);
|
|
221
|
-
};
|
|
222
|
-
for (const entry of state.pendingEvents) add(entry.event);
|
|
223
|
-
for (const deferred of state.pendingApplyDeferreds.values()) add(deferred.event);
|
|
224
|
-
return referenced;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function pruneStaleManualApplyEvidence(cwd = process.cwd()) {
|
|
228
|
-
const dir = manualApplyEvidenceDir(cwd);
|
|
229
|
-
if (!fs.existsSync(dir)) return [];
|
|
230
|
-
const referenced = referencedManualApplyEvidencePaths(cwd);
|
|
231
|
-
const removed = [];
|
|
232
|
-
for (const name of fs.readdirSync(dir)) {
|
|
233
|
-
if (!name.endsWith('.json')) continue;
|
|
234
|
-
const fullPath = path.join(dir, name);
|
|
235
|
-
if (referenced.has(fullPath)) continue;
|
|
236
|
-
try {
|
|
237
|
-
fs.unlinkSync(fullPath);
|
|
238
|
-
removed.push(fullPath);
|
|
239
|
-
} catch {
|
|
240
|
-
// Stale evidence cleanup is best-effort; Apply verification never relies
|
|
241
|
-
// on deleting these files.
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
return removed;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function compactManualApplyBatch(batch = {}) {
|
|
248
|
-
const entries = (batch.entries || []).map(compactManualApplyEntry);
|
|
249
|
-
const candidates = compactManualApplyCandidates(batch.candidates || []);
|
|
250
|
-
return {
|
|
251
|
-
version: batch.version,
|
|
252
|
-
pageUrl: batch.pageUrl || null,
|
|
253
|
-
count: batch.count,
|
|
254
|
-
entries,
|
|
255
|
-
ops: entries.flatMap((entry) => entry.ops.map((op) => ({ ...op, entryId: entry.id }))),
|
|
256
|
-
candidates: candidates.length > 0 ? candidates : undefined,
|
|
257
|
-
context: batch.context ? {
|
|
258
|
-
bufferPath: batch.context.bufferPath,
|
|
259
|
-
totalEntries: batch.context.totalEntries,
|
|
260
|
-
totalOps: batch.context.totalOps,
|
|
261
|
-
chunkIndex: batch.context.chunkIndex,
|
|
262
|
-
chunkTotal: batch.context.chunkTotal,
|
|
263
|
-
totalApplyOps: batch.context.totalApplyOps,
|
|
264
|
-
} : undefined,
|
|
265
|
-
};
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function compactManualApplyCandidates(candidates) {
|
|
269
|
-
return (Array.isArray(candidates) ? candidates : [])
|
|
270
|
-
.slice(0, 24)
|
|
271
|
-
.map((candidate) => ({
|
|
272
|
-
entryId: candidate.entryId,
|
|
273
|
-
ref: candidate.ref,
|
|
274
|
-
sourceHint: compactManualApplySourceMatch(candidate.sourceHint),
|
|
275
|
-
textMatches: compactManualApplySourceMatches(candidate.textMatches, 8),
|
|
276
|
-
objectKeyMatches: compactManualApplySourceMatches(candidate.objectKeyMatches, 8),
|
|
277
|
-
contextTextMatches: compactManualApplySourceMatches(candidate.contextTextMatches, 8),
|
|
278
|
-
locatorMatches: compactManualApplySourceMatches(candidate.locatorMatches, 6),
|
|
279
|
-
}));
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function compactManualApplySourceMatches(matches, limit) {
|
|
283
|
-
return (Array.isArray(matches) ? matches : [])
|
|
284
|
-
.slice(0, limit)
|
|
285
|
-
.map(compactManualApplySourceMatch)
|
|
286
|
-
.filter(Boolean);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function compactManualApplySourceMatch(match) {
|
|
290
|
-
if (!match || typeof match !== 'object') return null;
|
|
291
|
-
const file = match.relativeFile || match.file;
|
|
292
|
-
if (!file && !match.line) return null;
|
|
293
|
-
return {
|
|
294
|
-
file: summarizeManualLogFile(file),
|
|
295
|
-
line: match.line || null,
|
|
296
|
-
column: match.column || null,
|
|
297
|
-
reason: match.reason || match.kind || undefined,
|
|
298
|
-
status: match.status || undefined,
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
function compactManualApplyEntry(entry = {}) {
|
|
303
|
-
return {
|
|
304
|
-
id: entry.id,
|
|
305
|
-
pageUrl: entry.pageUrl,
|
|
306
|
-
stagedAt: entry.stagedAt || null,
|
|
307
|
-
element: compactManualApplyContext(entry.element),
|
|
308
|
-
ops: (entry.ops || []).map(compactManualApplyOp),
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function compactManualApplyOp(op = {}) {
|
|
313
|
-
return {
|
|
314
|
-
entryId: op.entryId,
|
|
315
|
-
ref: op.ref,
|
|
316
|
-
contextRef: op.contextRef,
|
|
317
|
-
tag: op.tag,
|
|
318
|
-
elementId: op.elementId,
|
|
319
|
-
classes: Array.isArray(op.classes) ? op.classes : [],
|
|
320
|
-
originalText: op.originalText,
|
|
321
|
-
newText: op.newText,
|
|
322
|
-
deleted: op.deleted === true || undefined,
|
|
323
|
-
sourceHint: op.sourceHint || null,
|
|
324
|
-
leaf: compactManualApplyContext(op.leaf),
|
|
325
|
-
nearbyEditableTexts: compactNearbyManualEditTexts(op.nearbyEditableTexts),
|
|
326
|
-
container: compactManualApplyContext(op.container),
|
|
327
|
-
contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 8) : undefined,
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
function compactManualApplyContext(value) {
|
|
332
|
-
if (!value || typeof value !== 'object') return null;
|
|
333
|
-
return {
|
|
334
|
-
ref: value.ref,
|
|
335
|
-
tagName: value.tagName || value.tag || null,
|
|
336
|
-
id: value.id || null,
|
|
337
|
-
classes: Array.isArray(value.classes) ? value.classes : [],
|
|
338
|
-
textContent: truncateManualApplyText(value.textContent, MANUAL_APPLY_COMPACT_TEXT_LIMIT),
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function compactNearbyManualEditTexts(items) {
|
|
343
|
-
return (Array.isArray(items) ? items : [])
|
|
344
|
-
.slice(0, MANUAL_APPLY_COMPACT_NEARBY_LIMIT)
|
|
345
|
-
.map((item) => typeof item === 'string' ? { text: truncateManualApplyText(item, MANUAL_APPLY_COMPACT_TEXT_LIMIT) } : {
|
|
346
|
-
ref: item?.ref,
|
|
347
|
-
tag: item?.tag,
|
|
348
|
-
classes: Array.isArray(item?.classes) ? item.classes : [],
|
|
349
|
-
text: truncateManualApplyText(item?.text, MANUAL_APPLY_COMPACT_TEXT_LIMIT),
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function truncateManualApplyText(value, max) {
|
|
354
|
-
if (typeof value !== 'string') return value || null;
|
|
355
|
-
return value.length > max ? value.slice(0, max) : value;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async function pushApplyBatchInChunksAndWait(batch, pageUrl, context = {}) {
|
|
359
|
-
const repair = context?.repair || batch?.repair || null;
|
|
360
|
-
if (repair) return pushApplyEventAndWait(batch, pageUrl, null, repair);
|
|
361
|
-
const chunks = splitManualApplyBatch(batch, manualEditApplyChunkSize());
|
|
362
|
-
if (chunks.length <= 1) return pushApplyEventAndWait(batch, pageUrl);
|
|
363
|
-
|
|
364
|
-
const expectedOpsByEntry = new Map();
|
|
365
|
-
for (const entry of batch?.entries || []) {
|
|
366
|
-
expectedOpsByEntry.set(entry.id, Array.isArray(entry.ops) ? entry.ops.length : 0);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const appliedOpsByEntry = new Map();
|
|
370
|
-
const failedByEntry = new Map();
|
|
371
|
-
const files = new Set();
|
|
372
|
-
const notes = [];
|
|
373
|
-
let aborted = false;
|
|
374
|
-
|
|
375
|
-
for (const chunk of chunks) {
|
|
376
|
-
if (aborted) {
|
|
377
|
-
markChunkEntriesFailed(failedByEntry, chunk, 'manual_edit_chunk_aborted');
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
let result;
|
|
382
|
-
try {
|
|
383
|
-
result = normalizeApplyChunkResult(await pushApplyEventAndWait(chunk.batch, pageUrl, chunk.meta));
|
|
384
|
-
} catch (err) {
|
|
385
|
-
markChunkEntriesFailed(failedByEntry, chunk, err.message || 'chat_agent_error');
|
|
386
|
-
aborted = true;
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
for (const file of result.files) files.add(file);
|
|
391
|
-
notes.push(...result.notes);
|
|
392
|
-
|
|
393
|
-
const chunkFailedIds = new Set();
|
|
394
|
-
for (const item of result.failed) {
|
|
395
|
-
const entryId = item.entryId || item.id;
|
|
396
|
-
if (!entryId) continue;
|
|
397
|
-
chunkFailedIds.add(entryId);
|
|
398
|
-
if (!failedByEntry.has(entryId)) {
|
|
399
|
-
failedByEntry.set(entryId, {
|
|
400
|
-
entryId,
|
|
401
|
-
reason: item.reason || item.message || 'failed',
|
|
402
|
-
candidates: Array.isArray(item.candidates) ? item.candidates : [],
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
if (result.status === 'error') {
|
|
408
|
-
markChunkEntriesFailed(failedByEntry, chunk, result.message || firstFailureReason(result) || 'chat_agent_error');
|
|
409
|
-
aborted = true;
|
|
410
|
-
continue;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
const reportedAppliedIds = new Set(result.appliedEntryIds);
|
|
414
|
-
for (const entryId of reportedAppliedIds) {
|
|
415
|
-
if (!chunk.entryIds.has(entryId) || chunkFailedIds.has(entryId)) continue;
|
|
416
|
-
appliedOpsByEntry.set(entryId, (appliedOpsByEntry.get(entryId) || 0) + (chunk.opCountsByEntry.get(entryId) || 0));
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
for (const entryId of chunk.entryIds) {
|
|
420
|
-
if (reportedAppliedIds.has(entryId) || chunkFailedIds.has(entryId)) continue;
|
|
421
|
-
if (!failedByEntry.has(entryId)) {
|
|
422
|
-
failedByEntry.set(entryId, { entryId, reason: 'not_reported_applied', candidates: [] });
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const appliedEntryIds = [];
|
|
428
|
-
for (const [entryId, expectedOps] of expectedOpsByEntry.entries()) {
|
|
429
|
-
if (failedByEntry.has(entryId)) continue;
|
|
430
|
-
if ((appliedOpsByEntry.get(entryId) || 0) === expectedOps && expectedOps > 0) {
|
|
431
|
-
appliedEntryIds.push(entryId);
|
|
432
|
-
} else if (!failedByEntry.has(entryId)) {
|
|
433
|
-
failedByEntry.set(entryId, { entryId, reason: 'not_reported_applied', candidates: [] });
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const failed = [...failedByEntry.values()];
|
|
438
|
-
return {
|
|
439
|
-
status: failed.length === 0 ? 'done' : appliedEntryIds.length > 0 ? 'partial' : 'error',
|
|
440
|
-
appliedEntryIds,
|
|
441
|
-
failed,
|
|
442
|
-
files: [...files],
|
|
443
|
-
notes,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function normalizeApplyChunkResult(result) {
|
|
448
|
-
const status = result?.status === 'partial' ? 'partial' : result?.status === 'error' ? 'error' : 'done';
|
|
449
|
-
return {
|
|
450
|
-
status,
|
|
451
|
-
message: typeof result?.message === 'string' ? result.message : null,
|
|
452
|
-
appliedEntryIds: Array.isArray(result?.appliedEntryIds) ? result.appliedEntryIds.filter((id) => typeof id === 'string') : [],
|
|
453
|
-
failed: Array.isArray(result?.failed) ? result.failed.filter(Boolean) : [],
|
|
454
|
-
files: Array.isArray(result?.files) ? result.files.filter((file) => typeof file === 'string') : [],
|
|
455
|
-
notes: Array.isArray(result?.notes) ? result.notes.filter((note) => typeof note === 'string') : [],
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
function manualApplyResultShapeHint(eventId = 'EVENT_ID') {
|
|
460
|
-
return `Use live-poll.mjs --reply ${eventId} done --data '{"status":"done","appliedEntryIds":["ENTRY_ID"],"failed":[],"files":["src/page.html"],"notes":[]}'`;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function invalidManualApplyResult(reason, eventId, extra = {}) {
|
|
464
|
-
return {
|
|
465
|
-
ok: false,
|
|
466
|
-
body: {
|
|
467
|
-
error: 'invalid_manual_apply_result',
|
|
468
|
-
reason,
|
|
469
|
-
hint: manualApplyResultShapeHint(eventId),
|
|
470
|
-
...extra,
|
|
471
|
-
},
|
|
472
|
-
};
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
function validateManualApplyResultMessage(msg, deferred) {
|
|
476
|
-
let data = msg?.data;
|
|
477
|
-
const eventId = msg?.id || deferred?.event?.id || 'EVENT_ID';
|
|
478
|
-
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
479
|
-
return invalidManualApplyResult('missing_result_data', eventId);
|
|
480
|
-
}
|
|
481
|
-
if ('entries' in data || 'ops' in data) {
|
|
482
|
-
return invalidManualApplyResult('summary_result_not_allowed', eventId);
|
|
483
|
-
}
|
|
484
|
-
if (!['done', 'partial', 'error'].includes(data.status)) {
|
|
485
|
-
return invalidManualApplyResult('invalid_status', eventId, { status: data.status ?? null });
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
for (const key of ['appliedEntryIds', 'failed', 'files', 'notes']) {
|
|
489
|
-
if (!Array.isArray(data[key])) {
|
|
490
|
-
return invalidManualApplyResult(`${key}_must_be_array`, eventId);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
for (const [index, value] of data.appliedEntryIds.entries()) {
|
|
495
|
-
if (typeof value !== 'string' || !value) {
|
|
496
|
-
return invalidManualApplyResult('appliedEntryIds_must_contain_strings', eventId, { index });
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
for (const [index, value] of data.files.entries()) {
|
|
500
|
-
if (typeof value !== 'string' || !value) {
|
|
501
|
-
return invalidManualApplyResult('files_must_contain_strings', eventId, { index });
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
for (const [index, value] of data.notes.entries()) {
|
|
505
|
-
if (typeof value !== 'string') {
|
|
506
|
-
return invalidManualApplyResult('notes_must_contain_strings', eventId, { index });
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
for (const [index, item] of data.failed.entries()) {
|
|
510
|
-
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
511
|
-
return invalidManualApplyResult('failed_must_contain_objects', eventId, { index });
|
|
512
|
-
}
|
|
513
|
-
if (typeof item.entryId !== 'string' || !item.entryId) {
|
|
514
|
-
return invalidManualApplyResult('failed_entryId_required', eventId, { index });
|
|
515
|
-
}
|
|
516
|
-
if (typeof item.reason !== 'string' || !item.reason) {
|
|
517
|
-
return invalidManualApplyResult('failed_reason_required', eventId, { index });
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const eventEntryIds = new Set((deferred?.batch?.entries || []).map((entry) => entry.id).filter(Boolean));
|
|
522
|
-
for (const entryId of data.appliedEntryIds) {
|
|
523
|
-
if (eventEntryIds.size > 0 && !eventEntryIds.has(entryId)) {
|
|
524
|
-
return invalidManualApplyResult('applied_entry_id_not_in_event', eventId, { entryId });
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
for (const item of data.failed) {
|
|
528
|
-
if (eventEntryIds.size > 0 && !eventEntryIds.has(item.entryId)) {
|
|
529
|
-
return invalidManualApplyResult('failed_entry_id_not_in_event', eventId, { entryId: item.entryId });
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
if (data.status === 'done') {
|
|
534
|
-
if (data.failed.length > 0) {
|
|
535
|
-
return invalidManualApplyResult('done_result_has_failed_entries', eventId);
|
|
536
|
-
}
|
|
537
|
-
if (countManualApplyOps(deferred?.batch) > 0 && data.appliedEntryIds.length === 0) {
|
|
538
|
-
return invalidManualApplyResult('done_result_missing_applied_entry_ids', eventId);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
if (data.status === 'partial' && data.appliedEntryIds.length === 0 && data.failed.length === 0) {
|
|
542
|
-
return invalidManualApplyResult('partial_result_has_no_entries', eventId);
|
|
543
|
-
}
|
|
544
|
-
if (data.status === 'error' && data.appliedEntryIds.length > 0) {
|
|
545
|
-
return invalidManualApplyResult('error_result_has_applied_entries', eventId);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
return {
|
|
549
|
-
ok: true,
|
|
550
|
-
result: {
|
|
551
|
-
status: data.status,
|
|
552
|
-
message: typeof data.message === 'string' ? data.message : undefined,
|
|
553
|
-
appliedEntryIds: data.appliedEntryIds,
|
|
554
|
-
failed: data.failed,
|
|
555
|
-
files: data.files,
|
|
556
|
-
notes: data.notes,
|
|
557
|
-
},
|
|
558
|
-
};
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
function firstFailureReason(result) {
|
|
562
|
-
const first = Array.isArray(result?.failed) ? result.failed.find(Boolean) : null;
|
|
563
|
-
return first?.reason || first?.message || null;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function markChunkEntriesFailed(failedByEntry, chunk, reason) {
|
|
567
|
-
for (const entryId of chunk.entryIds) {
|
|
568
|
-
if (failedByEntry.has(entryId)) continue;
|
|
569
|
-
failedByEntry.set(entryId, { entryId, reason, candidates: [] });
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
function splitManualApplyBatch(batch, maxOps) {
|
|
574
|
-
const totalOpCount = countManualApplyOps(batch);
|
|
575
|
-
if (totalOpCount <= maxOps) {
|
|
576
|
-
return [{
|
|
577
|
-
batch,
|
|
578
|
-
meta: null,
|
|
579
|
-
entryIds: new Set((batch?.entries || []).map((entry) => entry.id).filter(Boolean)),
|
|
580
|
-
opCountsByEntry: new Map((batch?.entries || []).map((entry) => [entry.id, Array.isArray(entry.ops) ? entry.ops.length : 0])),
|
|
581
|
-
}];
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const rawChunks = [];
|
|
585
|
-
let current = createManualApplyChunkBuilder();
|
|
586
|
-
for (const entry of batch?.entries || []) {
|
|
587
|
-
const ops = entry.ops || [];
|
|
588
|
-
if (ops.length <= maxOps) {
|
|
589
|
-
if (current.opCount > 0 && current.opCount + ops.length > maxOps) {
|
|
590
|
-
rawChunks.push(current);
|
|
591
|
-
current = createManualApplyChunkBuilder();
|
|
592
|
-
}
|
|
593
|
-
for (const op of ops) addOpToManualApplyChunk(current, entry, op);
|
|
594
|
-
continue;
|
|
595
|
-
}
|
|
596
|
-
if (current.opCount > 0) {
|
|
597
|
-
rawChunks.push(current);
|
|
598
|
-
current = createManualApplyChunkBuilder();
|
|
599
|
-
}
|
|
600
|
-
for (const op of ops) {
|
|
601
|
-
if (current.opCount >= maxOps) {
|
|
602
|
-
rawChunks.push(current);
|
|
603
|
-
current = createManualApplyChunkBuilder();
|
|
604
|
-
}
|
|
605
|
-
addOpToManualApplyChunk(current, entry, op);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
if (current.opCount > 0) rawChunks.push(current);
|
|
609
|
-
|
|
610
|
-
return rawChunks.map((chunk, index) => ({
|
|
611
|
-
batch: {
|
|
612
|
-
...batch,
|
|
613
|
-
count: chunk.opCount,
|
|
614
|
-
entries: chunk.entries,
|
|
615
|
-
ops: chunk.ops,
|
|
616
|
-
candidates: filterManualApplyChunkCandidates(batch, chunk.refsByEntry),
|
|
617
|
-
context: {
|
|
618
|
-
...(batch?.context || {}),
|
|
619
|
-
totalEntries: chunk.entries.length,
|
|
620
|
-
totalOps: chunk.opCount,
|
|
621
|
-
chunkIndex: index + 1,
|
|
622
|
-
chunkTotal: rawChunks.length,
|
|
623
|
-
totalApplyOps: totalOpCount,
|
|
624
|
-
},
|
|
625
|
-
},
|
|
626
|
-
meta: {
|
|
627
|
-
index: index + 1,
|
|
628
|
-
total: rawChunks.length,
|
|
629
|
-
opCount: chunk.opCount,
|
|
630
|
-
totalOpCount,
|
|
631
|
-
},
|
|
632
|
-
entryIds: new Set(chunk.entries.map((entry) => entry.id).filter(Boolean)),
|
|
633
|
-
opCountsByEntry: chunk.opCountsByEntry,
|
|
634
|
-
}));
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function createManualApplyChunkBuilder() {
|
|
638
|
-
return {
|
|
639
|
-
entries: [],
|
|
640
|
-
entryById: new Map(),
|
|
641
|
-
entryIds: new Set(),
|
|
642
|
-
ops: [],
|
|
643
|
-
refsByEntry: new Map(),
|
|
644
|
-
opCountsByEntry: new Map(),
|
|
645
|
-
opCount: 0,
|
|
646
|
-
};
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
function addOpToManualApplyChunk(chunk, entry, op) {
|
|
650
|
-
let chunkEntry = chunk.entryById.get(entry.id);
|
|
651
|
-
if (!chunkEntry) {
|
|
652
|
-
chunkEntry = { ...entry, ops: [] };
|
|
653
|
-
chunk.entryById.set(entry.id, chunkEntry);
|
|
654
|
-
chunk.entryIds.add(entry.id);
|
|
655
|
-
chunk.entries.push(chunkEntry);
|
|
656
|
-
}
|
|
657
|
-
chunkEntry.ops.push(op);
|
|
658
|
-
chunk.ops.push({ ...op, entryId: op.entryId || entry.id });
|
|
659
|
-
if (!chunk.refsByEntry.has(entry.id)) chunk.refsByEntry.set(entry.id, new Set());
|
|
660
|
-
if (op.ref) chunk.refsByEntry.get(entry.id).add(op.ref);
|
|
661
|
-
chunk.opCountsByEntry.set(entry.id, (chunk.opCountsByEntry.get(entry.id) || 0) + 1);
|
|
662
|
-
chunk.opCount += 1;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
function filterManualApplyChunkCandidates(batch, refsByEntry) {
|
|
666
|
-
return (batch?.candidates || []).filter((candidate) => {
|
|
667
|
-
const refs = refsByEntry.get(candidate.entryId);
|
|
668
|
-
if (!refs) return false;
|
|
669
|
-
if (!candidate.ref) return true;
|
|
670
|
-
return refs.has(candidate.ref);
|
|
671
|
-
});
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
function resolveApplyDeferred(eventId, body) {
|
|
675
|
-
const deferred = state.pendingApplyDeferreds.get(eventId);
|
|
676
|
-
if (!deferred) return false;
|
|
677
|
-
state.pendingApplyDeferreds.delete(eventId);
|
|
678
|
-
clearTimeout(deferred.timer);
|
|
679
|
-
removeManualApplyEvidence(deferred.event?.evidencePath);
|
|
680
|
-
deferred.resolve(body);
|
|
681
|
-
return true;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
function rejectApplyDeferred(eventId, reason) {
|
|
685
|
-
const deferred = state.pendingApplyDeferreds.get(eventId);
|
|
686
|
-
if (!deferred) return false;
|
|
687
|
-
state.pendingApplyDeferreds.delete(eventId);
|
|
688
|
-
clearTimeout(deferred.timer);
|
|
689
|
-
removeManualApplyEvidence(deferred.event?.evidencePath);
|
|
690
|
-
deferred.reject(new Error(reason || 'chat_agent_error'));
|
|
691
|
-
return true;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function snapshotApplyEventFiles(batch) {
|
|
695
|
-
const snapshot = new Map();
|
|
696
|
-
for (const relativeFile of collectManualApplyFiles(batch)) {
|
|
697
|
-
const absolute = path.resolve(process.cwd(), relativeFile);
|
|
698
|
-
try {
|
|
699
|
-
snapshot.set(relativeFile, {
|
|
700
|
-
exists: fs.existsSync(absolute),
|
|
701
|
-
content: fs.existsSync(absolute) ? fs.readFileSync(absolute, 'utf-8') : '',
|
|
702
|
-
});
|
|
703
|
-
} catch {
|
|
704
|
-
// If a file cannot be read before dispatch, do not attempt late rollback.
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
return snapshot;
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
function manualApplyTransactionPath(cwd = process.cwd()) {
|
|
711
|
-
return path.join(getLiveDir(cwd), 'manual-edit-apply-transaction.json');
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function readManualApplyTransaction(cwd = process.cwd()) {
|
|
715
|
-
const file = manualApplyTransactionPath(cwd);
|
|
716
|
-
if (!fs.existsSync(file)) return null;
|
|
717
|
-
try {
|
|
718
|
-
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
719
|
-
} catch {
|
|
720
|
-
return null;
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
|
|
724
|
-
function writeManualApplyTransaction({ cwd = process.cwd(), pageUrl = null, batch }) {
|
|
725
|
-
const file = manualApplyTransactionPath(cwd);
|
|
726
|
-
const files = collectManualApplyFiles(batch);
|
|
727
|
-
const transaction = {
|
|
728
|
-
version: 1,
|
|
729
|
-
id: randomUUID().replace(/-/g, '').slice(0, 8),
|
|
730
|
-
createdAt: new Date().toISOString(),
|
|
731
|
-
pageUrl,
|
|
732
|
-
entryIds: (batch?.entries || []).map((entry) => entry.id).filter(Boolean),
|
|
733
|
-
files: files.map((relativeFile) => {
|
|
734
|
-
const absolute = path.resolve(cwd, relativeFile);
|
|
735
|
-
const exists = fs.existsSync(absolute);
|
|
736
|
-
return {
|
|
737
|
-
file: relativeFile,
|
|
738
|
-
exists,
|
|
739
|
-
content: exists ? fs.readFileSync(absolute, 'utf-8') : '',
|
|
740
|
-
};
|
|
741
|
-
}),
|
|
742
|
-
};
|
|
743
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
744
|
-
fs.writeFileSync(`${file}.tmp`, JSON.stringify(transaction, null, 2) + '\n', 'utf-8');
|
|
745
|
-
fs.renameSync(`${file}.tmp`, file);
|
|
746
|
-
return transaction;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
function clearManualApplyTransaction(cwd = process.cwd(), transactionId = null) {
|
|
750
|
-
const file = manualApplyTransactionPath(cwd);
|
|
751
|
-
if (!fs.existsSync(file)) return false;
|
|
752
|
-
if (transactionId) {
|
|
753
|
-
const existing = readManualApplyTransaction(cwd);
|
|
754
|
-
if (existing?.id && existing.id !== transactionId) return false;
|
|
755
|
-
}
|
|
756
|
-
try {
|
|
757
|
-
fs.unlinkSync(file);
|
|
758
|
-
return true;
|
|
759
|
-
} catch {
|
|
760
|
-
return false;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
function rollbackManualApplyTransaction({ cwd = process.cwd(), pageUrl = null, reason = 'manual_edit_transaction_rollback' } = {}) {
|
|
765
|
-
const transaction = readManualApplyTransaction(cwd);
|
|
766
|
-
if (!transaction) return null;
|
|
767
|
-
if (pageUrl && transaction.pageUrl && transaction.pageUrl !== pageUrl) return null;
|
|
768
|
-
|
|
769
|
-
let pendingIds = new Set();
|
|
770
|
-
try {
|
|
771
|
-
const buffer = readManualEditsBuffer(cwd);
|
|
772
|
-
pendingIds = new Set((buffer.entries || []).map((entry) => entry.id).filter(Boolean));
|
|
773
|
-
} catch {
|
|
774
|
-
pendingIds = new Set(transaction.entryIds || []);
|
|
775
|
-
}
|
|
776
|
-
const shouldRollback = (transaction.entryIds || []).some((id) => pendingIds.has(id));
|
|
777
|
-
if (!shouldRollback) {
|
|
778
|
-
clearManualApplyTransaction(cwd, transaction.id);
|
|
779
|
-
return { id: transaction.id, reason, rolledBackFiles: [], rollbackFailures: [], skipped: 'entries_not_pending' };
|
|
780
|
-
}
|
|
781
|
-
|
|
782
|
-
const rolledBackFiles = [];
|
|
783
|
-
const rollbackFailures = [];
|
|
784
|
-
for (const item of transaction.files || []) {
|
|
785
|
-
const relativeFile = normalizeProjectFile(item.file);
|
|
786
|
-
if (!relativeFile) continue;
|
|
787
|
-
const absolute = path.resolve(cwd, relativeFile);
|
|
788
|
-
try {
|
|
789
|
-
if (item.exists) {
|
|
790
|
-
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
791
|
-
fs.writeFileSync(absolute, item.content || '', 'utf-8');
|
|
792
|
-
} else if (fs.existsSync(absolute)) {
|
|
793
|
-
fs.rmSync(absolute);
|
|
794
|
-
}
|
|
795
|
-
rolledBackFiles.push(relativeFile);
|
|
796
|
-
} catch (err) {
|
|
797
|
-
rollbackFailures.push({ file: relativeFile, reason: 'restore_failed', message: err.message || String(err) });
|
|
798
|
-
}
|
|
799
|
-
}
|
|
800
|
-
clearManualApplyTransaction(cwd, transaction.id);
|
|
801
|
-
recordManualEditActivity('manual_edit_transaction_rolled_back', {
|
|
802
|
-
id: transaction.id,
|
|
803
|
-
pageUrl: transaction.pageUrl || null,
|
|
804
|
-
reason,
|
|
805
|
-
entryIds: transaction.entryIds || [],
|
|
806
|
-
rolledBackFiles: rolledBackFiles.map(summarizeManualLogFile).filter(Boolean),
|
|
807
|
-
rollbackFailures: summarizeManualDiagnostics(rollbackFailures),
|
|
808
|
-
});
|
|
809
|
-
return { id: transaction.id, reason, rolledBackFiles, rollbackFailures };
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
function collectManualApplyFiles(batch, extraFiles = []) {
|
|
813
|
-
const files = [];
|
|
814
|
-
for (const entry of batch?.entries || []) {
|
|
815
|
-
for (const op of entry.ops || []) files.push(op.sourceHint?.file);
|
|
816
|
-
}
|
|
817
|
-
for (const candidate of batch?.candidates || []) {
|
|
818
|
-
files.push(candidate.sourceHint?.relativeFile, candidate.sourceHint?.file);
|
|
819
|
-
for (const item of candidate.textMatches || []) files.push(item.file);
|
|
820
|
-
for (const item of candidate.objectKeyMatches || []) files.push(item.file);
|
|
821
|
-
for (const item of candidate.locatorMatches || []) files.push(item.file);
|
|
822
|
-
for (const item of candidate.contextTextMatches || []) files.push(item.file);
|
|
823
|
-
}
|
|
824
|
-
files.push(...(extraFiles || []));
|
|
825
|
-
return [...new Set(files)]
|
|
826
|
-
.map((file) => normalizeProjectFile(file))
|
|
827
|
-
.filter(Boolean);
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function normalizeProjectFile(file) {
|
|
831
|
-
if (!file || typeof file !== 'string') return null;
|
|
832
|
-
const absolute = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file);
|
|
833
|
-
const relative = path.relative(process.cwd(), absolute);
|
|
834
|
-
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;
|
|
835
|
-
return relative;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
function rollbackApplySnapshot(batch, rollbackSnapshot, extraFiles = [], reason = 'manual_edit_apply_snapshot_rollback') {
|
|
839
|
-
const scope = collectManualApplyFiles(batch, extraFiles);
|
|
840
|
-
const rolledBackFiles = [];
|
|
841
|
-
const rollbackFailures = [];
|
|
842
|
-
for (const relativeFile of scope) {
|
|
843
|
-
const before = rollbackSnapshot?.get(relativeFile);
|
|
844
|
-
if (!before) continue;
|
|
845
|
-
const absolute = path.resolve(process.cwd(), relativeFile);
|
|
846
|
-
try {
|
|
847
|
-
if (before.exists) {
|
|
848
|
-
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
849
|
-
fs.writeFileSync(absolute, before.content, 'utf-8');
|
|
850
|
-
} else if (fs.existsSync(absolute)) {
|
|
851
|
-
fs.rmSync(absolute);
|
|
852
|
-
}
|
|
853
|
-
rolledBackFiles.push(relativeFile);
|
|
854
|
-
} catch (err) {
|
|
855
|
-
rollbackFailures.push({ file: relativeFile, reason: 'restore_failed', message: err.message || String(err) });
|
|
856
|
-
}
|
|
857
|
-
}
|
|
858
|
-
return { rolledBackFiles, rollbackFailures };
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
function rollbackTimedOutApplyReply(msg) {
|
|
862
|
-
const details = state.timedOutApplyIds.get(msg.id);
|
|
863
|
-
if (!details) return { rolledBackFiles: [], rollbackFailures: [] };
|
|
864
|
-
state.timedOutApplyIds.delete(msg.id);
|
|
865
|
-
return rollbackApplySnapshot(details.batch, details.rollbackSnapshot, msg.data?.files || [], 'stale_manual_edit_apply_reply');
|
|
866
|
-
}
|
|
867
|
-
|
|
868
141
|
// Cap per-annotation upload size. A full 1920×1080 PNG is typically <1 MB;
|
|
869
142
|
// cap at 10 MB to guard against runaway writes from a misbehaving client.
|
|
870
143
|
const MAX_ANNOTATION_BYTES = 10 * 1024 * 1024;
|
|
@@ -897,6 +170,8 @@ function leaseEvent(entry, leaseMs) {
|
|
|
897
170
|
return entry.event;
|
|
898
171
|
}
|
|
899
172
|
entry.leaseUntil = Date.now() + leaseMs;
|
|
173
|
+
scheduleLeaseFlush();
|
|
174
|
+
broadcastAgentPollingIfChanged();
|
|
900
175
|
return entry.event;
|
|
901
176
|
}
|
|
902
177
|
|
|
@@ -907,33 +182,14 @@ function acknowledgePendingEvent(id) {
|
|
|
907
182
|
const acknowledged = state.pendingEvents[idx].event;
|
|
908
183
|
state.pendingEvents.splice(idx, 1);
|
|
909
184
|
scheduleLeaseFlush();
|
|
185
|
+
broadcastAgentPollingIfChanged();
|
|
910
186
|
return acknowledged;
|
|
911
187
|
}
|
|
912
188
|
|
|
913
|
-
function
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
function buildManualApplyAgentAction(eventOrId = 'EVENT_ID') {
|
|
919
|
-
return {
|
|
920
|
-
kind: 'manual_edit_apply',
|
|
921
|
-
required: 'apply_source_edits_then_reply',
|
|
922
|
-
replyCommand: manualApplyReplyCommand(eventOrId),
|
|
923
|
-
warning: 'Polling only leases this work item; it does not commit source edits.',
|
|
924
|
-
};
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
function summarizeManualApplyEvent(event = {}, batch = event.batch) {
|
|
928
|
-
const entries = Array.isArray(batch?.entries) ? batch.entries : [];
|
|
929
|
-
const opCount = entries.reduce((sum, entry) => sum + (Array.isArray(entry.ops) ? entry.ops.length : 0), 0);
|
|
930
|
-
return {
|
|
931
|
-
pageUrl: event.pageUrl || null,
|
|
932
|
-
chunk: event.chunk || null,
|
|
933
|
-
entryCount: entries.length,
|
|
934
|
-
opCount,
|
|
935
|
-
files: collectManualApplyFiles(batch),
|
|
936
|
-
};
|
|
189
|
+
function findPendingEventById(id) {
|
|
190
|
+
if (!id) return null;
|
|
191
|
+
const entry = state.pendingEvents.find((item) => item.event?.id === id);
|
|
192
|
+
return entry?.event || null;
|
|
937
193
|
}
|
|
938
194
|
|
|
939
195
|
function summarizePendingEventForStatus(entry) {
|
|
@@ -949,51 +205,46 @@ function summarizePendingEventForStatus(entry) {
|
|
|
949
205
|
summary.chunk = event.chunk || null;
|
|
950
206
|
summary.repair = event.repair || null;
|
|
951
207
|
summary.evidencePath = event.evidencePath || null;
|
|
952
|
-
summary.agentAction = event.agentAction ||
|
|
953
|
-
summary.manualApplySummary =
|
|
208
|
+
summary.agentAction = event.agentAction || manualApply.buildAgentAction(event);
|
|
209
|
+
summary.manualApplySummary = manualApply.summarizeEvent(event, manualApply.getDeferred(event.id)?.batch || event.batch);
|
|
954
210
|
}
|
|
955
211
|
return summary;
|
|
956
212
|
}
|
|
957
213
|
|
|
958
|
-
function
|
|
959
|
-
|
|
960
|
-
|
|
214
|
+
function summarizeActiveSessionForClient(snapshot = {}) {
|
|
215
|
+
return {
|
|
216
|
+
id: snapshot.id,
|
|
217
|
+
phase: snapshot.phase,
|
|
218
|
+
pageUrl: snapshot.pageUrl ?? null,
|
|
219
|
+
sourceFile: snapshot.sourceFile ?? null,
|
|
220
|
+
previewFile: snapshot.previewFile ?? null,
|
|
221
|
+
previewMode: snapshot.previewMode ?? null,
|
|
222
|
+
expectedVariants: snapshot.expectedVariants ?? 0,
|
|
223
|
+
arrivedVariants: snapshot.arrivedVariants ?? 0,
|
|
224
|
+
visibleVariant: snapshot.visibleVariant ?? null,
|
|
225
|
+
checkpointRevision: snapshot.checkpointRevision ?? 0,
|
|
226
|
+
paramValues: snapshot.paramValues || {},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
961
229
|
|
|
230
|
+
function activeSessionSummaries() {
|
|
231
|
+
if (!state.sessionStore) return [];
|
|
232
|
+
return state.sessionStore.listActiveSessions().map((snapshot) => summarizeActiveSessionForClient(snapshot));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function cancelQueuedAnonymousExitEvents() {
|
|
236
|
+
let removed = 0;
|
|
962
237
|
for (let i = state.pendingEvents.length - 1; i >= 0; i -= 1) {
|
|
963
238
|
const event = state.pendingEvents[i]?.event;
|
|
964
|
-
if (
|
|
239
|
+
if (event?.type !== 'exit' || event.id) continue;
|
|
965
240
|
state.pendingEvents.splice(i, 1);
|
|
966
|
-
|
|
967
|
-
canceledById.set(event.id, {
|
|
968
|
-
id: event.id,
|
|
969
|
-
pageUrl: event.pageUrl,
|
|
970
|
-
entryCount: event.batch?.entries?.length || 0,
|
|
971
|
-
});
|
|
241
|
+
removed += 1;
|
|
972
242
|
}
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
state.pendingApplyDeferreds.delete(eventId);
|
|
977
|
-
clearTimeout(deferred.timer);
|
|
978
|
-
const rollback = rollbackApplySnapshot(deferred.batch, deferred.rollbackSnapshot, [], reason);
|
|
979
|
-
tombstoneTimedOutApplyId(eventId, {
|
|
980
|
-
batch: deferred.batch,
|
|
981
|
-
rollbackSnapshot: deferred.rollbackSnapshot,
|
|
982
|
-
reason,
|
|
983
|
-
});
|
|
984
|
-
removeManualApplyEvidence(deferred.event?.evidencePath);
|
|
985
|
-
canceledById.set(eventId, {
|
|
986
|
-
id: eventId,
|
|
987
|
-
pageUrl: deferred.pageUrl,
|
|
988
|
-
entryCount: deferred.batch?.entries?.length || 0,
|
|
989
|
-
rolledBackFiles: rollback.rolledBackFiles,
|
|
990
|
-
rollbackFailures: rollback.rollbackFailures,
|
|
991
|
-
});
|
|
992
|
-
deferred.reject(new Error(reason));
|
|
243
|
+
if (removed > 0) {
|
|
244
|
+
scheduleLeaseFlush();
|
|
245
|
+
broadcastAgentPollingIfChanged();
|
|
993
246
|
}
|
|
994
|
-
|
|
995
|
-
if (canceledById.size > 0) flushPendingPolls();
|
|
996
|
-
return [...canceledById.values()];
|
|
247
|
+
return removed;
|
|
997
248
|
}
|
|
998
249
|
|
|
999
250
|
function scheduleLeaseFlush() {
|
|
@@ -1001,7 +252,6 @@ function scheduleLeaseFlush() {
|
|
|
1001
252
|
clearTimeout(state.leaseTimer);
|
|
1002
253
|
state.leaseTimer = null;
|
|
1003
254
|
}
|
|
1004
|
-
if (state.pendingPolls.length === 0) return;
|
|
1005
255
|
const now = Date.now();
|
|
1006
256
|
const nextLeaseUntil = state.pendingEvents
|
|
1007
257
|
.map((entry) => entry.leaseUntil || 0)
|
|
@@ -1011,7 +261,8 @@ function scheduleLeaseFlush() {
|
|
|
1011
261
|
state.leaseTimer = setTimeout(() => {
|
|
1012
262
|
state.leaseTimer = null;
|
|
1013
263
|
flushPendingPolls();
|
|
1014
|
-
|
|
264
|
+
broadcastAgentPollingIfChanged();
|
|
265
|
+
}, Math.max(0, nextLeaseUntil - now + POLL_LEASE_EXPIRY_TIMER_GRACE_MS));
|
|
1015
266
|
}
|
|
1016
267
|
|
|
1017
268
|
function flushPendingPolls() {
|
|
@@ -1032,7 +283,9 @@ function flushPendingPolls() {
|
|
|
1032
283
|
}
|
|
1033
284
|
|
|
1034
285
|
function agentPollingConnected() {
|
|
1035
|
-
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
return state.pendingPolls.length > 0
|
|
288
|
+
|| state.pendingEvents.some((entry) => entry.leaseUntil && entry.leaseUntil > now);
|
|
1036
289
|
}
|
|
1037
290
|
|
|
1038
291
|
function broadcastAgentPollingIfChanged() {
|
|
@@ -1085,61 +338,6 @@ function getManualEditStatus() {
|
|
|
1085
338
|
}
|
|
1086
339
|
}
|
|
1087
340
|
|
|
1088
|
-
function summarizePendingManualEditBatch(pageUrl = null) {
|
|
1089
|
-
try {
|
|
1090
|
-
const buffer = readManualEditsBuffer(process.cwd());
|
|
1091
|
-
const entries = (buffer.entries || [])
|
|
1092
|
-
.filter((entry) => !pageUrl || entry.pageUrl === pageUrl);
|
|
1093
|
-
return {
|
|
1094
|
-
pendingEntryCount: entries.length,
|
|
1095
|
-
pendingOpCount: entries.reduce((sum, entry) => sum + (entry.ops?.length || 0), 0),
|
|
1096
|
-
};
|
|
1097
|
-
} catch (err) {
|
|
1098
|
-
return { pendingSummaryError: err.message || String(err) };
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
function summarizeManualApplyFailures(failed) {
|
|
1103
|
-
if (!Array.isArray(failed)) return [];
|
|
1104
|
-
return failed.slice(0, 20).map((item) => ({
|
|
1105
|
-
id: item.id || item.entryId || null,
|
|
1106
|
-
reason: item.reason || item.message || 'failed',
|
|
1107
|
-
message: compactManualLogText(item.message, 300),
|
|
1108
|
-
files: Array.isArray(item.files) ? item.files.slice(0, 12).map(summarizeManualLogFile).filter(Boolean) : undefined,
|
|
1109
|
-
checks: summarizeManualDiagnostics(item.checks),
|
|
1110
|
-
failures: summarizeManualDiagnostics(item.failures),
|
|
1111
|
-
candidates: summarizeManualDiagnostics(item.candidates),
|
|
1112
|
-
}));
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
function summarizeManualDiagnostics(items) {
|
|
1116
|
-
if (!Array.isArray(items) || items.length === 0) return undefined;
|
|
1117
|
-
return items.slice(0, 12).map((item) => ({
|
|
1118
|
-
reason: item.reason || item.kind || undefined,
|
|
1119
|
-
detail: compactManualLogText(item.detail, 220),
|
|
1120
|
-
message: compactManualLogText(item.message, 300),
|
|
1121
|
-
file: summarizeManualLogFile(item.file || item.relativeFile),
|
|
1122
|
-
line: item.line || undefined,
|
|
1123
|
-
ref: compactManualLogText(item.ref, 180),
|
|
1124
|
-
marker: compactManualLogText(item.marker, 120),
|
|
1125
|
-
files: Array.isArray(item.files) ? item.files.slice(0, 8).map(summarizeManualLogFile).filter(Boolean) : undefined,
|
|
1126
|
-
}));
|
|
1127
|
-
}
|
|
1128
|
-
|
|
1129
|
-
function summarizeManualLogFile(file) {
|
|
1130
|
-
if (!file || typeof file !== 'string') return undefined;
|
|
1131
|
-
if (!path.isAbsolute(file)) return file;
|
|
1132
|
-
const relative = path.relative(process.cwd(), file);
|
|
1133
|
-
return relative && !relative.startsWith('..') && !path.isAbsolute(relative) ? relative : file;
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
function compactManualLogText(value, max = 200) {
|
|
1137
|
-
if (typeof value !== 'string') return undefined;
|
|
1138
|
-
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
1139
|
-
if (normalized.length <= max) return normalized;
|
|
1140
|
-
return normalized.slice(0, max) + `... [truncated ${normalized.length - max} chars]`;
|
|
1141
|
-
}
|
|
1142
|
-
|
|
1143
341
|
// ---------------------------------------------------------------------------
|
|
1144
342
|
// Load scripts
|
|
1145
343
|
// ---------------------------------------------------------------------------
|
|
@@ -1159,29 +357,25 @@ function loadBrowserScripts() {
|
|
|
1159
357
|
try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ }
|
|
1160
358
|
}
|
|
1161
359
|
|
|
1162
|
-
//
|
|
1163
|
-
// can re-read on
|
|
1164
|
-
// should land on the next tab reload, not require a server restart.
|
|
1165
|
-
const
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
}
|
|
360
|
+
// Browser script parts: DO NOT cache. Return paths so the /live.js handler
|
|
361
|
+
// can re-read every part on each request. Editing browser code during
|
|
362
|
+
// iteration should land on the next tab reload, not require a server restart.
|
|
363
|
+
const liveScriptParts = resolveLiveBrowserScriptParts(__dirname);
|
|
364
|
+
try {
|
|
365
|
+
assertLiveBrowserScriptParts(liveScriptParts);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
process.stderr.write('Error: ' + err.message + '\n');
|
|
368
|
+
process.exit(1);
|
|
1172
369
|
}
|
|
1173
370
|
|
|
1174
|
-
return { detectScript,
|
|
371
|
+
return { detectScript, liveScriptParts };
|
|
1175
372
|
}
|
|
1176
373
|
|
|
1177
374
|
function hasProjectContext() {
|
|
1178
375
|
// PRODUCT.md carries brand voice / anti-references — that's what determines
|
|
1179
376
|
// whether variants are brand-aware. DESIGN.md (visual tokens) is a separate
|
|
1180
377
|
// concern, surfaced by the design panel's own empty state.
|
|
1181
|
-
|
|
1182
|
-
fs.accessSync(path.join(CONTEXT_DIR, 'PRODUCT.md'), fs.constants.R_OK);
|
|
1183
|
-
return true;
|
|
1184
|
-
} catch { return false; }
|
|
378
|
+
return !!PROJECT_CONTEXT.hasProduct;
|
|
1185
379
|
}
|
|
1186
380
|
|
|
1187
381
|
function statOrNull(filePath) {
|
|
@@ -1191,7 +385,7 @@ function statOrNull(filePath) {
|
|
|
1191
385
|
// HTTP request handler
|
|
1192
386
|
// ---------------------------------------------------------------------------
|
|
1193
387
|
|
|
1194
|
-
function createRequestHandler({ detectScript,
|
|
388
|
+
function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
1195
389
|
return (req, res) => {
|
|
1196
390
|
const url = new URL(req.url, `http://localhost:${state.port}`);
|
|
1197
391
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -1207,21 +401,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
|
|
1207
401
|
// the next tab reload. No-store headers prevent browser caching across
|
|
1208
402
|
// sessions — during iteration, a cached old script silently breaks
|
|
1209
403
|
// every subsequent session.
|
|
1210
|
-
let
|
|
1211
|
-
let liveScript;
|
|
404
|
+
let parts;
|
|
1212
405
|
try {
|
|
1213
|
-
|
|
1214
|
-
liveScript = fs.readFileSync(livePath, 'utf-8');
|
|
406
|
+
parts = readLiveBrowserScriptParts(liveScriptParts);
|
|
1215
407
|
} catch (err) {
|
|
1216
408
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1217
409
|
res.end('Error reading live browser scripts: ' + err.message);
|
|
1218
410
|
return;
|
|
1219
411
|
}
|
|
1220
|
-
const body =
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
412
|
+
const body = assembleLiveBrowserScript({
|
|
413
|
+
token: state.token,
|
|
414
|
+
port: state.port,
|
|
415
|
+
vocabulary: LIVE_COMMANDS,
|
|
416
|
+
parts,
|
|
417
|
+
});
|
|
1225
418
|
res.writeHead(200, {
|
|
1226
419
|
'Content-Type': 'application/javascript',
|
|
1227
420
|
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
|
@@ -1318,7 +511,7 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
|
|
1318
511
|
if (p === '/status') {
|
|
1319
512
|
const token = url.searchParams.get('token');
|
|
1320
513
|
if (token !== state.token) { res.writeHead(401, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ error: 'Unauthorized' })); return; }
|
|
1321
|
-
const sessions =
|
|
514
|
+
const sessions = activeSessionSummaries();
|
|
1322
515
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1323
516
|
res.end(JSON.stringify({
|
|
1324
517
|
status: 'ok',
|
|
@@ -1357,8 +550,8 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
|
|
1357
550
|
const token = url.searchParams.get('token');
|
|
1358
551
|
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
1359
552
|
|
|
1360
|
-
const mdPath =
|
|
1361
|
-
const jsonPath = resolveDesignSidecarPath(process.cwd(), CONTEXT_DIR) || getDesignSidecarPath(process.cwd());
|
|
553
|
+
const mdPath = DESIGN_MD_PATH;
|
|
554
|
+
const jsonPath = resolveDesignSidecarPath(process.cwd(), PROJECT_CONTEXT.designContextDir || CONTEXT_DIR) || getDesignSidecarPath(process.cwd());
|
|
1362
555
|
const mdStat = statOrNull(mdPath);
|
|
1363
556
|
const jsonStat = statOrNull(jsonPath);
|
|
1364
557
|
|
|
@@ -1423,6 +616,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
|
|
1423
616
|
if (p === '/events' && req.method === 'GET') {
|
|
1424
617
|
const token = url.searchParams.get('token');
|
|
1425
618
|
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
619
|
+
clearTimeout(state.exitTimer);
|
|
620
|
+
state.exitTimer = null;
|
|
621
|
+
cancelQueuedAnonymousExitEvents();
|
|
1426
622
|
res.writeHead(200, {
|
|
1427
623
|
'Content-Type': 'text/event-stream',
|
|
1428
624
|
'Cache-Control': 'no-cache',
|
|
@@ -1432,10 +628,10 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
|
|
1432
628
|
type: 'connected',
|
|
1433
629
|
hasProjectContext: hasProjectContext(),
|
|
1434
630
|
agentPolling: agentPollingConnected(),
|
|
631
|
+
activeSessions: activeSessionSummaries(),
|
|
1435
632
|
}) + '\n\n');
|
|
1436
633
|
|
|
1437
634
|
state.sseClients.add(res);
|
|
1438
|
-
clearTimeout(state.exitTimer);
|
|
1439
635
|
|
|
1440
636
|
// Keepalive: SSE comment every 30s prevents silent connection drops.
|
|
1441
637
|
const heartbeat = setInterval(() => {
|
|
@@ -1455,334 +651,7 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
|
|
1455
651
|
return;
|
|
1456
652
|
}
|
|
1457
653
|
|
|
1458
|
-
|
|
1459
|
-
// page batch through the local AI copy-edit runner.
|
|
1460
|
-
if (p === '/manual-edit-stash' && req.method === 'POST') {
|
|
1461
|
-
let body = '';
|
|
1462
|
-
req.on('data', (c) => { body += c; });
|
|
1463
|
-
req.on('end', () => {
|
|
1464
|
-
let msg;
|
|
1465
|
-
try { msg = JSON.parse(body); } catch {
|
|
1466
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1467
|
-
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
1468
|
-
return;
|
|
1469
|
-
}
|
|
1470
|
-
if (msg.token !== state.token) {
|
|
1471
|
-
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1472
|
-
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
1473
|
-
return;
|
|
1474
|
-
}
|
|
1475
|
-
const error = validateEvent({ ...msg, type: 'manual_edits' });
|
|
1476
|
-
if (error) {
|
|
1477
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1478
|
-
res.end(JSON.stringify({ error }));
|
|
1479
|
-
return;
|
|
1480
|
-
}
|
|
1481
|
-
try {
|
|
1482
|
-
stageManualEditEntry(process.cwd(), {
|
|
1483
|
-
id: msg.id,
|
|
1484
|
-
pageUrl: msg.pageUrl,
|
|
1485
|
-
element: msg.element,
|
|
1486
|
-
ops: msg.ops,
|
|
1487
|
-
});
|
|
1488
|
-
} catch (err) {
|
|
1489
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1490
|
-
res.end(JSON.stringify({ error: 'stash_write_failed', message: err.message }));
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
const { totalCount, perPage } = countPendingByPage(process.cwd());
|
|
1494
|
-
const pendingCount = perPage[msg.pageUrl] || 0;
|
|
1495
|
-
recordManualEditActivity('manual_edit_stashed', {
|
|
1496
|
-
id: msg.id,
|
|
1497
|
-
pageUrl: msg.pageUrl,
|
|
1498
|
-
opCount: msg.ops.length,
|
|
1499
|
-
pendingCount,
|
|
1500
|
-
totalCount,
|
|
1501
|
-
hintedFileCount: new Set((msg.ops || []).map((op) => summarizeManualLogFile(op.sourceHint?.file)).filter(Boolean)).size,
|
|
1502
|
-
});
|
|
1503
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1504
|
-
res.end(JSON.stringify({ ok: true, pendingCount, totalCount, perPage }));
|
|
1505
|
-
});
|
|
1506
|
-
return;
|
|
1507
|
-
}
|
|
1508
|
-
|
|
1509
|
-
// GET /manual-edit-stash?pageUrl=<url> → { count, totalCount, perPage, entries }
|
|
1510
|
-
if (p === '/manual-edit-stash' && req.method === 'GET') {
|
|
1511
|
-
const token = url.searchParams.get('token');
|
|
1512
|
-
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
1513
|
-
const pageUrl = url.searchParams.get('pageUrl') || '';
|
|
1514
|
-
const { totalCount, perPage } = countPendingByPage(process.cwd());
|
|
1515
|
-
const buffer = readManualEditsBuffer(process.cwd());
|
|
1516
|
-
const entriesForPage = pageUrl ? buffer.entries.filter((e) => e.pageUrl === pageUrl) : buffer.entries;
|
|
1517
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1518
|
-
res.end(JSON.stringify({
|
|
1519
|
-
count: pageUrl ? (perPage[pageUrl] || 0) : totalCount,
|
|
1520
|
-
totalCount,
|
|
1521
|
-
perPage,
|
|
1522
|
-
entries: entriesForPage,
|
|
1523
|
-
}));
|
|
1524
|
-
return;
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
// POST /manual-edit-commit?pageUrl=<url> → ask the AI to apply the staged page batch.
|
|
1528
|
-
if (p === '/manual-edit-commit' && req.method === 'POST') {
|
|
1529
|
-
const token = url.searchParams.get('token');
|
|
1530
|
-
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
1531
|
-
const pageUrl = url.searchParams.get('pageUrl');
|
|
1532
|
-
const asyncMode = /^(1|true|yes)$/i.test(url.searchParams.get('async') || '');
|
|
1533
|
-
const repairOnly = /^(1|true|yes)$/i.test(url.searchParams.get('repair') || '');
|
|
1534
|
-
const existingTransaction = readManualApplyTransaction(process.cwd());
|
|
1535
|
-
if (repairOnly && !existingTransaction) {
|
|
1536
|
-
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
1537
|
-
res.end(JSON.stringify({ error: 'manual_edit_repair_transaction_missing' }));
|
|
1538
|
-
return;
|
|
1539
|
-
}
|
|
1540
|
-
const recoveredTransaction = repairOnly ? null : rollbackManualApplyTransaction({
|
|
1541
|
-
cwd: process.cwd(),
|
|
1542
|
-
pageUrl,
|
|
1543
|
-
reason: 'manual_edit_commit_recovered_abandoned_transaction',
|
|
1544
|
-
});
|
|
1545
|
-
const before = getManualEditStatus();
|
|
1546
|
-
const pendingCount = pageUrl ? (before.perPage[pageUrl] || 0) : before.totalCount;
|
|
1547
|
-
recordManualEditActivity('manual_edit_commit_started', {
|
|
1548
|
-
pageUrl,
|
|
1549
|
-
repairOnly,
|
|
1550
|
-
pendingCount,
|
|
1551
|
-
totalCount: before.totalCount,
|
|
1552
|
-
recoveredTransaction: recoveredTransaction ? {
|
|
1553
|
-
id: recoveredTransaction.id,
|
|
1554
|
-
reason: recoveredTransaction.reason,
|
|
1555
|
-
skipped: recoveredTransaction.skipped,
|
|
1556
|
-
rolledBackFiles: recoveredTransaction.rolledBackFiles,
|
|
1557
|
-
rollbackFailures: summarizeManualDiagnostics(recoveredTransaction.rollbackFailures),
|
|
1558
|
-
} : null,
|
|
1559
|
-
...summarizePendingManualEditBatch(pageUrl),
|
|
1560
|
-
});
|
|
1561
|
-
if (asyncMode) {
|
|
1562
|
-
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
1563
|
-
res.end(JSON.stringify({
|
|
1564
|
-
status: 'started',
|
|
1565
|
-
pendingCount,
|
|
1566
|
-
totalCount: before.totalCount,
|
|
1567
|
-
perPage: before.perPage,
|
|
1568
|
-
}));
|
|
1569
|
-
}
|
|
1570
|
-
(async () => {
|
|
1571
|
-
let result;
|
|
1572
|
-
let routedProvider = 'subprocess';
|
|
1573
|
-
let transaction = null;
|
|
1574
|
-
let commitBatch = null;
|
|
1575
|
-
try {
|
|
1576
|
-
if (pendingCount > 0) {
|
|
1577
|
-
const transactionBatch = buildManualEditEvidence({ cwd: process.cwd(), pageUrl });
|
|
1578
|
-
commitBatch = transactionBatch;
|
|
1579
|
-
if (!repairOnly && countManualApplyOps(transactionBatch) > 0) {
|
|
1580
|
-
transaction = writeManualApplyTransaction({
|
|
1581
|
-
cwd: process.cwd(),
|
|
1582
|
-
pageUrl,
|
|
1583
|
-
batch: transactionBatch,
|
|
1584
|
-
});
|
|
1585
|
-
} else if (repairOnly && existingTransaction) {
|
|
1586
|
-
transaction = existingTransaction;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
const requestedMode = (process.env.IMPECCABLE_LIVE_COPY_AGENT || 'auto').trim().toLowerCase();
|
|
1590
|
-
const useChatRoute = requestedMode === 'chat'
|
|
1591
|
-
|| (requestedMode === 'auto' && chatAgentLikelyActive());
|
|
1592
|
-
if (useChatRoute) {
|
|
1593
|
-
routedProvider = 'chat';
|
|
1594
|
-
const timeoutMs = Number(process.env.IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS || 120000);
|
|
1595
|
-
result = await commitManualEdits({
|
|
1596
|
-
cwd: process.cwd(),
|
|
1597
|
-
pageUrl,
|
|
1598
|
-
provider: 'chat',
|
|
1599
|
-
env: process.env,
|
|
1600
|
-
timeoutMs,
|
|
1601
|
-
chatAvailable: chatAgentLikelyActive,
|
|
1602
|
-
applyBatchToSource: (batch, context) => pushApplyBatchInChunksAndWait(batch, pageUrl, context),
|
|
1603
|
-
repairOnly,
|
|
1604
|
-
transactionId: transaction?.id || existingTransaction?.id || null,
|
|
1605
|
-
batch: commitBatch,
|
|
1606
|
-
});
|
|
1607
|
-
} else {
|
|
1608
|
-
const timeoutMs = Number(process.env.IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS || 120000);
|
|
1609
|
-
const provider = ['codex', 'claude', 'mock'].includes(requestedMode) ? requestedMode : undefined;
|
|
1610
|
-
result = await commitManualEdits({
|
|
1611
|
-
cwd: process.cwd(),
|
|
1612
|
-
pageUrl,
|
|
1613
|
-
provider,
|
|
1614
|
-
env: process.env,
|
|
1615
|
-
timeoutMs,
|
|
1616
|
-
chatAvailable: chatAgentLikelyActive,
|
|
1617
|
-
repairOnly,
|
|
1618
|
-
transactionId: transaction?.id || existingTransaction?.id || null,
|
|
1619
|
-
batch: commitBatch,
|
|
1620
|
-
});
|
|
1621
|
-
}
|
|
1622
|
-
} catch (err) {
|
|
1623
|
-
if (transaction) {
|
|
1624
|
-
rollbackManualApplyTransaction({
|
|
1625
|
-
cwd: process.cwd(),
|
|
1626
|
-
pageUrl,
|
|
1627
|
-
reason: 'manual_edit_commit_exception',
|
|
1628
|
-
});
|
|
1629
|
-
}
|
|
1630
|
-
const message = err.stderr?.toString?.() || err.message;
|
|
1631
|
-
recordManualEditActivity('manual_edit_commit_failed', {
|
|
1632
|
-
pageUrl,
|
|
1633
|
-
provider: routedProvider,
|
|
1634
|
-
error: 'manual_edit_commit_failed',
|
|
1635
|
-
message,
|
|
1636
|
-
transactionId: transaction?.id || null,
|
|
1637
|
-
});
|
|
1638
|
-
if (!asyncMode) {
|
|
1639
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1640
|
-
res.end(JSON.stringify({
|
|
1641
|
-
error: 'manual_edit_commit_failed',
|
|
1642
|
-
message,
|
|
1643
|
-
}));
|
|
1644
|
-
}
|
|
1645
|
-
return;
|
|
1646
|
-
} finally {
|
|
1647
|
-
if (transaction) {
|
|
1648
|
-
const shouldKeepTransaction = result?.needsManualDecision === true;
|
|
1649
|
-
if (!shouldKeepTransaction) clearManualApplyTransaction(process.cwd(), transaction.id);
|
|
1650
|
-
}
|
|
1651
|
-
}
|
|
1652
|
-
const { totalCount, perPage } = countPendingByPage(process.cwd());
|
|
1653
|
-
if (result?.needsManualDecision) {
|
|
1654
|
-
recordManualEditActivity('manual_edit_repair_needs_decision', {
|
|
1655
|
-
pageUrl,
|
|
1656
|
-
provider: routedProvider,
|
|
1657
|
-
transactionId: transaction?.id || existingTransaction?.id || null,
|
|
1658
|
-
repair: result.repair || null,
|
|
1659
|
-
failed: summarizeManualApplyFailures(result.failed),
|
|
1660
|
-
files: Array.isArray(result.files) ? result.files.slice(0, 20).map(summarizeManualLogFile).filter(Boolean) : [],
|
|
1661
|
-
remainingCount: pageUrl ? (perPage[pageUrl] || 0) : totalCount,
|
|
1662
|
-
totalCount,
|
|
1663
|
-
});
|
|
1664
|
-
} else {
|
|
1665
|
-
recordManualEditActivity('manual_edit_commit_done', {
|
|
1666
|
-
pageUrl,
|
|
1667
|
-
provider: routedProvider,
|
|
1668
|
-
reason: result.reason || null,
|
|
1669
|
-
repair: result.repair || null,
|
|
1670
|
-
appliedCount: Array.isArray(result.applied) ? result.applied.length : 0,
|
|
1671
|
-
failedCount: Array.isArray(result.failed) ? result.failed.length : 0,
|
|
1672
|
-
failed: summarizeManualApplyFailures(result.failed),
|
|
1673
|
-
files: Array.isArray(result.files) ? result.files.slice(0, 20).map(summarizeManualLogFile).filter(Boolean) : [],
|
|
1674
|
-
warnings: summarizeManualDiagnostics(result.warnings),
|
|
1675
|
-
rolledBackFiles: Array.isArray(result.rolledBackFiles) ? result.rolledBackFiles.slice(0, 20).map(summarizeManualLogFile).filter(Boolean) : [],
|
|
1676
|
-
rollbackFailures: summarizeManualDiagnostics(result.rollbackFailures),
|
|
1677
|
-
unreportedFiles: Array.isArray(result.unreportedFiles) ? result.unreportedFiles.slice(0, 20).map(summarizeManualLogFile).filter(Boolean) : undefined,
|
|
1678
|
-
noteCount: Array.isArray(result.notes) ? result.notes.length : 0,
|
|
1679
|
-
cleared: result.cleared || 0,
|
|
1680
|
-
remainingCount: pageUrl ? (perPage[pageUrl] || 0) : totalCount,
|
|
1681
|
-
totalCount,
|
|
1682
|
-
});
|
|
1683
|
-
}
|
|
1684
|
-
if (!asyncMode) {
|
|
1685
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1686
|
-
res.end(JSON.stringify({ ...result, totalCount, perPage }));
|
|
1687
|
-
}
|
|
1688
|
-
})();
|
|
1689
|
-
return;
|
|
1690
|
-
}
|
|
1691
|
-
|
|
1692
|
-
// POST /manual-edit-repair-decision → user resolves an exhausted repair loop.
|
|
1693
|
-
if (p === '/manual-edit-repair-decision' && req.method === 'POST') {
|
|
1694
|
-
let body = '';
|
|
1695
|
-
req.on('data', (chunk) => { body += chunk; });
|
|
1696
|
-
req.on('end', () => {
|
|
1697
|
-
let payload = {};
|
|
1698
|
-
try { payload = body ? JSON.parse(body) : {}; } catch {
|
|
1699
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1700
|
-
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
const token = payload.token || url.searchParams.get('token');
|
|
1704
|
-
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
1705
|
-
const pageUrl = payload.pageUrl || url.searchParams.get('pageUrl') || null;
|
|
1706
|
-
const action = String(payload.action || url.searchParams.get('action') || '').trim().toLowerCase();
|
|
1707
|
-
if (action !== 'rollback') {
|
|
1708
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1709
|
-
res.end(JSON.stringify({ error: 'unsupported_manual_edit_repair_decision', action }));
|
|
1710
|
-
return;
|
|
1711
|
-
}
|
|
1712
|
-
const rollback = rollbackManualApplyTransaction({
|
|
1713
|
-
cwd: process.cwd(),
|
|
1714
|
-
pageUrl,
|
|
1715
|
-
reason: 'manual_edit_user_requested_rollback',
|
|
1716
|
-
});
|
|
1717
|
-
const { totalCount, perPage } = countPendingByPage(process.cwd());
|
|
1718
|
-
const response = {
|
|
1719
|
-
action,
|
|
1720
|
-
pageUrl,
|
|
1721
|
-
rollback,
|
|
1722
|
-
remainingCount: pageUrl ? (perPage[pageUrl] || 0) : totalCount,
|
|
1723
|
-
totalCount,
|
|
1724
|
-
perPage,
|
|
1725
|
-
};
|
|
1726
|
-
recordManualEditActivity('manual_edit_repair_rollback_done', response);
|
|
1727
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1728
|
-
res.end(JSON.stringify(response));
|
|
1729
|
-
});
|
|
1730
|
-
return;
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
// POST /manual-edit-discard?pageUrl=<url> → drops entries (all if no pageUrl)
|
|
1734
|
-
if (p === '/manual-edit-discard' && req.method === 'POST') {
|
|
1735
|
-
const token = url.searchParams.get('token');
|
|
1736
|
-
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
1737
|
-
const pageUrl = url.searchParams.get('pageUrl');
|
|
1738
|
-
let discarded;
|
|
1739
|
-
let discardedEntries = [];
|
|
1740
|
-
let canceledApplyEvents = [];
|
|
1741
|
-
let transactionRollback = null;
|
|
1742
|
-
try {
|
|
1743
|
-
const buffer = readManualEditsBuffer(process.cwd());
|
|
1744
|
-
transactionRollback = rollbackManualApplyTransaction({
|
|
1745
|
-
cwd: process.cwd(),
|
|
1746
|
-
pageUrl,
|
|
1747
|
-
reason: 'manual_edit_discarded',
|
|
1748
|
-
});
|
|
1749
|
-
if (pageUrl) {
|
|
1750
|
-
discardedEntries = buffer.entries.filter((entry) => entry.pageUrl === pageUrl);
|
|
1751
|
-
discarded = removeManualEditEntries(process.cwd(), (entry) => entry.pageUrl === pageUrl);
|
|
1752
|
-
} else {
|
|
1753
|
-
discardedEntries = buffer.entries;
|
|
1754
|
-
discarded = truncateManualEditsBuffer(process.cwd());
|
|
1755
|
-
}
|
|
1756
|
-
canceledApplyEvents = cancelPendingManualApplyEvents(pageUrl);
|
|
1757
|
-
} catch (err) {
|
|
1758
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1759
|
-
res.end(JSON.stringify({ error: 'discard_failed', message: err.message }));
|
|
1760
|
-
return;
|
|
1761
|
-
}
|
|
1762
|
-
const { totalCount, perPage } = countPendingByPage(process.cwd());
|
|
1763
|
-
recordManualEditActivity('manual_edit_discarded', {
|
|
1764
|
-
pageUrl,
|
|
1765
|
-
discarded,
|
|
1766
|
-
canceledApplyIds: canceledApplyEvents.map((event) => event.id),
|
|
1767
|
-
transactionRollback: transactionRollback ? {
|
|
1768
|
-
id: transactionRollback.id,
|
|
1769
|
-
rolledBackFiles: transactionRollback.rolledBackFiles?.map(summarizeManualLogFile).filter(Boolean) || [],
|
|
1770
|
-
rollbackFailures: summarizeManualDiagnostics(transactionRollback.rollbackFailures),
|
|
1771
|
-
skipped: transactionRollback.skipped,
|
|
1772
|
-
} : undefined,
|
|
1773
|
-
totalCount,
|
|
1774
|
-
});
|
|
1775
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1776
|
-
res.end(JSON.stringify({ discarded, entries: discardedEntries, canceledApplyEvents, totalCount, perPage }));
|
|
1777
|
-
return;
|
|
1778
|
-
}
|
|
1779
|
-
|
|
1780
|
-
// Defense in depth: redirect any stragglers from the old /manual-edit endpoint.
|
|
1781
|
-
if (p === '/manual-edit' && req.method === 'POST') {
|
|
1782
|
-
res.writeHead(410, { 'Content-Type': 'application/json' });
|
|
1783
|
-
res.end(JSON.stringify({ error: '/manual-edit is removed; use /manual-edit-stash and /manual-edit-commit for staged copy edits.' }));
|
|
1784
|
-
return;
|
|
1785
|
-
}
|
|
654
|
+
if (manualEditRoutes(req, res, url)) return;
|
|
1786
655
|
|
|
1787
656
|
// --- Browser→server events (replaces WebSocket messages) ---
|
|
1788
657
|
if (p === '/events' && req.method === 'POST') {
|
|
@@ -1827,6 +696,9 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
|
|
1827
696
|
return;
|
|
1828
697
|
}
|
|
1829
698
|
}
|
|
699
|
+
if (msg.type === 'exit') {
|
|
700
|
+
cleanupSvelteComponentSessionsBeforeExit();
|
|
701
|
+
}
|
|
1830
702
|
if (msg.type !== 'checkpoint') {
|
|
1831
703
|
enqueueEvent(msg);
|
|
1832
704
|
}
|
|
@@ -1872,16 +744,7 @@ function handlePollGet(req, res, url) {
|
|
|
1872
744
|
return;
|
|
1873
745
|
}
|
|
1874
746
|
state.lastPollAt = Date.now();
|
|
1875
|
-
// Bound the client-supplied long-poll timeout so a user-provided value cannot
|
|
1876
|
-
// schedule an unbounded timer (resource exhaustion).
|
|
1877
747
|
const timeout = parseInt(url.searchParams.get('timeout') || DEFAULT_POLL_TIMEOUT, 10);
|
|
1878
|
-
if (!Number.isFinite(timeout) || timeout < 0 || timeout > 300000) {
|
|
1879
|
-
// Reject out-of-range long-poll lifetimes so a user-provided value cannot
|
|
1880
|
-
// schedule an unbounded timer (resource exhaustion).
|
|
1881
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1882
|
-
res.end(JSON.stringify({ error: 'Invalid timeout' }));
|
|
1883
|
-
return;
|
|
1884
|
-
}
|
|
1885
748
|
const leaseMs = parseInt(url.searchParams.get('leaseMs') || '30000', 10);
|
|
1886
749
|
const available = findAvailablePendingEvent();
|
|
1887
750
|
if (available) {
|
|
@@ -1914,6 +777,36 @@ function handlePollGet(req, res, url) {
|
|
|
1914
777
|
});
|
|
1915
778
|
}
|
|
1916
779
|
|
|
780
|
+
function sessionFileMetadataFromPollReply(file) {
|
|
781
|
+
if (!file || typeof file !== 'string') return { file };
|
|
782
|
+
const normalized = file.split(path.sep).join('/');
|
|
783
|
+
const base = { file: normalized };
|
|
784
|
+
if (!normalized.endsWith('/manifest.json') && normalized !== 'manifest.json') return base;
|
|
785
|
+
if (!normalized.includes('node_modules/.impeccable-live/') && !normalized.includes('src/lib/impeccable/')) return base;
|
|
786
|
+
|
|
787
|
+
let full;
|
|
788
|
+
try {
|
|
789
|
+
full = path.resolve(process.cwd(), normalized);
|
|
790
|
+
const rel = path.relative(process.cwd(), full);
|
|
791
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return base;
|
|
792
|
+
} catch {
|
|
793
|
+
return base;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
try {
|
|
797
|
+
const manifest = JSON.parse(fs.readFileSync(full, 'utf-8'));
|
|
798
|
+
if (manifest?.previewMode !== 'svelte-component' || !manifest.sourceFile) return base;
|
|
799
|
+
return {
|
|
800
|
+
file: String(manifest.sourceFile).split(path.sep).join('/'),
|
|
801
|
+
sourceFile: String(manifest.sourceFile).split(path.sep).join('/'),
|
|
802
|
+
previewFile: normalized,
|
|
803
|
+
previewMode: 'svelte-component',
|
|
804
|
+
};
|
|
805
|
+
} catch {
|
|
806
|
+
return base;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
1917
810
|
function handlePollPost(req, res) {
|
|
1918
811
|
let body = '';
|
|
1919
812
|
req.on('data', (c) => { body += c; });
|
|
@@ -1929,9 +822,9 @@ function handlePollPost(req, res) {
|
|
|
1929
822
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
1930
823
|
return;
|
|
1931
824
|
}
|
|
1932
|
-
const pendingApplyDeferred =
|
|
825
|
+
const pendingApplyDeferred = manualApply.getDeferred(msg.id);
|
|
1933
826
|
if (pendingApplyDeferred) {
|
|
1934
|
-
const validation =
|
|
827
|
+
const validation = manualApply.validateResultMessage(msg, pendingApplyDeferred);
|
|
1935
828
|
if (!validation.ok) {
|
|
1936
829
|
recordManualEditActivity('manual_edit_apply_reply_invalid', {
|
|
1937
830
|
id: msg.id,
|
|
@@ -1956,15 +849,15 @@ function handlePollPost(req, res) {
|
|
|
1956
849
|
fileCount: validation.result.files.length,
|
|
1957
850
|
noteCount: validation.result.notes.length,
|
|
1958
851
|
});
|
|
1959
|
-
|
|
852
|
+
manualApply.resolveDeferred(msg.id, validation.result);
|
|
1960
853
|
acknowledgePendingEvent(msg.id);
|
|
1961
854
|
flushPendingPolls();
|
|
1962
855
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1963
856
|
res.end(JSON.stringify({ ok: true }));
|
|
1964
857
|
return;
|
|
1965
858
|
}
|
|
1966
|
-
if (
|
|
1967
|
-
const rollback =
|
|
859
|
+
if (manualApply.hasTimedOutId(msg.id)) {
|
|
860
|
+
const rollback = manualApply.rollbackTimedOutReply(msg);
|
|
1968
861
|
recordManualEditActivity('manual_edit_apply_stale_reply_rejected', {
|
|
1969
862
|
id: msg.id,
|
|
1970
863
|
rolledBackFileCount: rollback.rolledBackFiles?.length || 0,
|
|
@@ -1974,6 +867,16 @@ function handlePollPost(req, res) {
|
|
|
1974
867
|
res.end(JSON.stringify({ error: 'stale_manual_edit_apply_reply', ...rollback }));
|
|
1975
868
|
return;
|
|
1976
869
|
}
|
|
870
|
+
const pendingEventBeforeAck = findPendingEventById(msg.id);
|
|
871
|
+
if (pendingEventBeforeAck?.type === 'steer' && msg.type === 'steer_done'
|
|
872
|
+
&& !msg.file && !(typeof msg.message === 'string' && msg.message.trim())) {
|
|
873
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
874
|
+
res.end(JSON.stringify({
|
|
875
|
+
error: 'steer_done_requires_file_or_message',
|
|
876
|
+
hint: 'Reply with --file after writing source, or include a message explaining an intentional no-op.',
|
|
877
|
+
}));
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
1977
880
|
const acknowledgedEvent = acknowledgePendingEvent(msg.id);
|
|
1978
881
|
let skipJournalReply = false;
|
|
1979
882
|
let existingSession = null;
|
|
@@ -1996,6 +899,7 @@ function handlePollPost(req, res) {
|
|
|
1996
899
|
}));
|
|
1997
900
|
return;
|
|
1998
901
|
}
|
|
902
|
+
const replyFileMeta = sessionFileMetadataFromPollReply(msg.file);
|
|
1999
903
|
if (state.sessionStore && msg.id && !skipJournalReply) {
|
|
2000
904
|
try {
|
|
2001
905
|
const eventType = msg.type === 'steer_done'
|
|
@@ -2010,7 +914,10 @@ function handlePollPost(req, res) {
|
|
|
2010
914
|
state.sessionStore.appendEvent({
|
|
2011
915
|
type: eventType,
|
|
2012
916
|
id: msg.id,
|
|
2013
|
-
file:
|
|
917
|
+
file: replyFileMeta.file,
|
|
918
|
+
sourceFile: replyFileMeta.sourceFile,
|
|
919
|
+
previewFile: replyFileMeta.previewFile,
|
|
920
|
+
previewMode: replyFileMeta.previewMode,
|
|
2014
921
|
message: msg.message,
|
|
2015
922
|
sourceEventType: acknowledgedEvent?.type,
|
|
2016
923
|
carbonize: msg.data?.carbonize === true,
|
|
@@ -2019,7 +926,16 @@ function handlePollPost(req, res) {
|
|
|
2019
926
|
}
|
|
2020
927
|
flushPendingPolls();
|
|
2021
928
|
// Forward the reply to the browser via SSE
|
|
2022
|
-
broadcast({
|
|
929
|
+
broadcast({
|
|
930
|
+
type: msg.type || 'done',
|
|
931
|
+
id: msg.id,
|
|
932
|
+
message: msg.message,
|
|
933
|
+
file: msg.file,
|
|
934
|
+
sourceFile: replyFileMeta.sourceFile,
|
|
935
|
+
previewFile: replyFileMeta.previewFile,
|
|
936
|
+
previewMode: replyFileMeta.previewMode,
|
|
937
|
+
data: msg.data,
|
|
938
|
+
});
|
|
2023
939
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2024
940
|
res.end(JSON.stringify({ ok: true }));
|
|
2025
941
|
});
|
|
@@ -2032,6 +948,7 @@ function handlePollPost(req, res) {
|
|
|
2032
948
|
let httpServer = null;
|
|
2033
949
|
|
|
2034
950
|
function shutdown() {
|
|
951
|
+
cleanupSvelteComponentSessionsBeforeExit();
|
|
2035
952
|
removeLiveServerInfo(process.cwd());
|
|
2036
953
|
if (state.leaseTimer) clearTimeout(state.leaseTimer);
|
|
2037
954
|
state.leaseTimer = null;
|
|
@@ -2046,6 +963,25 @@ function shutdown() {
|
|
|
2046
963
|
process.exit(0);
|
|
2047
964
|
}
|
|
2048
965
|
|
|
966
|
+
function cleanupSvelteComponentSessionsBeforeExit() {
|
|
967
|
+
try {
|
|
968
|
+
removeAllSvelteComponentSessions(process.cwd());
|
|
969
|
+
} catch (err) {
|
|
970
|
+
console.warn('[impeccable] Svelte component session cleanup failed:', err.message);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
function applyLegacyDeferredAcceptsOnStartup() {
|
|
975
|
+
try {
|
|
976
|
+
const result = applyDeferredSvelteComponentAccepts(process.cwd());
|
|
977
|
+
if (result.applied > 0 || result.failed > 0) {
|
|
978
|
+
console.log('[impeccable] applied legacy deferred Svelte component accepts:', JSON.stringify(result));
|
|
979
|
+
}
|
|
980
|
+
} catch (err) {
|
|
981
|
+
console.warn('[impeccable] legacy deferred Svelte component accept apply failed:', err.message);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
2049
985
|
// ---------------------------------------------------------------------------
|
|
2050
986
|
// Main
|
|
2051
987
|
// ---------------------------------------------------------------------------
|
|
@@ -2167,12 +1103,12 @@ if (existingRecord?.info) {
|
|
|
2167
1103
|
|
|
2168
1104
|
state.token = randomUUID();
|
|
2169
1105
|
state.sessionStore = createLiveSessionStore({ cwd: process.cwd() });
|
|
2170
|
-
|
|
2171
|
-
cwd: process.cwd(),
|
|
1106
|
+
manualApply.rollbackTransaction({
|
|
2172
1107
|
reason: 'manual_edit_server_start_recovered_abandoned_transaction',
|
|
2173
1108
|
});
|
|
1109
|
+
applyLegacyDeferredAcceptsOnStartup();
|
|
2174
1110
|
restorePendingEventsFromStore();
|
|
2175
|
-
|
|
1111
|
+
manualApply.pruneStaleEvidence();
|
|
2176
1112
|
const portArg = args.find(a => a.startsWith('--port='));
|
|
2177
1113
|
state.port = portArg ? parseInt(portArg.split('=')[1], 10) : await findOpenPort();
|
|
2178
1114
|
// Annotation screenshots live in the project root so the agent's Read tool
|
|
@@ -2182,8 +1118,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd());
|
|
|
2182
1118
|
fs.mkdirSync(annotRoot, { recursive: true });
|
|
2183
1119
|
state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-'));
|
|
2184
1120
|
|
|
2185
|
-
const { detectScript,
|
|
2186
|
-
httpServer = http.createServer(createRequestHandler({ detectScript,
|
|
1121
|
+
const { detectScript, liveScriptParts } = loadBrowserScripts();
|
|
1122
|
+
httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts }));
|
|
2187
1123
|
|
|
2188
1124
|
httpServer.listen(state.port, '127.0.0.1', () => {
|
|
2189
1125
|
writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token });
|