quiver-cli 0.6.0 → 0.7.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/LICENSE +21 -0
- package/dist/cli.js +125 -43
- package/package.json +1 -1
- package/template/.agents/skills/agent-browser/SKILL.md +4 -9
- package/template/.agents/skills/apps/skybridge/SKILL.md +1 -1
- package/template/.agents/skills/code/improve/SKILL.md +9 -5
- package/template/.agents/skills/code/improve/references/audit-playbook.md +10 -10
- package/template/.agents/skills/code/improve/references/closing-the-loop.md +4 -3
- package/template/.agents/skills/code/improve/references/plan-template.md +5 -0
- package/template/.agents/skills/design/impeccable/SKILL.md +14 -24
- package/template/.agents/skills/design/impeccable/reference/animate.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/bolder.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/brand.md +2 -2
- package/template/.agents/skills/design/impeccable/reference/colorize.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/critique.md +6 -6
- package/template/.agents/skills/design/impeccable/reference/delight.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/distill.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/document.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/extract.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/hooks.md +90 -0
- package/template/.agents/skills/design/impeccable/reference/init.md +5 -5
- package/template/.agents/skills/design/impeccable/reference/live.md +16 -16
- package/template/.agents/skills/design/impeccable/reference/overdrive.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/polish.md +2 -2
- package/template/.agents/skills/design/impeccable/reference/quieter.md +1 -1
- package/template/.agents/skills/design/impeccable/reference/shape.md +2 -2
- package/template/.agents/skills/design/impeccable/scripts/context-signals.mjs +1 -1
- package/template/.agents/skills/design/impeccable/scripts/context.mjs +724 -33
- package/template/.agents/skills/design/impeccable/scripts/critique-storage.mjs +1 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/browser/injected/index.mjs +204 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/cli/main.mjs +57 -11
- package/template/.agents/skills/design/impeccable/scripts/detector/design-system.mjs +750 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns-browser.js +633 -46
- package/template/.agents/skills/design/impeccable/scripts/detector/detect-antipatterns.mjs +7 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/browser/detect-url.mjs +29 -4
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/regex/detect-text.mjs +43 -10
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/css-cascade.mjs +29 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/engines/static-html/detect-html.mjs +27 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/node/file-system.mjs +1 -1
- package/template/.agents/skills/design/impeccable/scripts/detector/registry/antipatterns.mjs +29 -0
- package/template/.agents/skills/design/impeccable/scripts/detector/rules/checks.mjs +401 -46
- package/template/.agents/skills/design/impeccable/scripts/detector/shared/inline-ignores.mjs +148 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-admin.mjs +661 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-before-edit.mjs +476 -0
- package/template/.agents/skills/design/impeccable/scripts/hook-lib.mjs +1632 -0
- package/template/.agents/skills/design/impeccable/scripts/hook.mjs +61 -0
- package/template/.agents/skills/design/impeccable/scripts/{design-parser.mjs → lib/design-parser.mjs} +8 -1
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-config.mjs +638 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/impeccable-paths.mjs +128 -0
- package/template/.agents/skills/design/impeccable/scripts/lib/target-args.mjs +42 -0
- package/template/.agents/skills/design/impeccable/scripts/live/browser-script-parts.mjs +49 -0
- package/template/.agents/skills/design/impeccable/scripts/{live-event-validation.mjs → live/event-validation.mjs} +6 -5
- package/template/.agents/skills/design/impeccable/scripts/live/manual-apply.mjs +939 -0
- package/template/.agents/skills/design/impeccable/scripts/live/manual-edit-routes.mjs +357 -0
- package/template/.agents/skills/design/impeccable/scripts/{live-manual-edits-buffer.mjs → live/manual-edits-buffer.mjs} +1 -1
- package/template/.agents/skills/design/impeccable/scripts/{live-session-store.mjs → live/session-store.mjs} +1 -1
- package/template/.agents/skills/design/impeccable/scripts/{live-ui-core.mjs → live/ui-core.mjs} +2 -1
- package/template/.agents/skills/design/impeccable/scripts/live/vocabulary.mjs +36 -0
- package/template/.agents/skills/design/impeccable/scripts/live-accept.mjs +3 -3
- package/template/.agents/skills/design/impeccable/scripts/live-browser-dom.js +146 -0
- package/template/.agents/skills/design/impeccable/scripts/live-browser.js +1456 -599
- package/template/.agents/skills/design/impeccable/scripts/live-commit-manual-edits.mjs +2 -2
- package/template/.agents/skills/design/impeccable/scripts/live-complete.mjs +2 -2
- package/template/.agents/skills/design/impeccable/scripts/live-discard-manual-edits.mjs +1 -1
- package/template/.agents/skills/design/impeccable/scripts/live-inject.mjs +35 -9
- package/template/.agents/skills/design/impeccable/scripts/live-insert.mjs +2 -2
- package/template/.agents/skills/design/impeccable/scripts/live-manual-edit-evidence.mjs +2 -2
- package/template/.agents/skills/design/impeccable/scripts/live-poll.mjs +18 -13
- package/template/.agents/skills/design/impeccable/scripts/live-resume.mjs +1 -1
- package/template/.agents/skills/design/impeccable/scripts/live-server.mjs +77 -1264
- package/template/.agents/skills/design/impeccable/scripts/live-status.mjs +2 -2
- package/template/.agents/skills/design/impeccable/scripts/live-target.mjs +30 -0
- package/template/.agents/skills/design/impeccable/scripts/live-wrap.mjs +4 -4
- package/template/.agents/skills/design/impeccable/scripts/live.mjs +73 -22
- package/template/.agents/skills/writing/humanizer/SKILL.md +621 -0
- package/template/.agents/upstreams.json +17 -8
- package/template/.agents/skills/apps/skybridge/references/architecture.md +0 -175
- package/template/.agents/skills/apps/skybridge/references/copy-template.md +0 -24
- package/template/.agents/skills/apps/skybridge/references/csp.md +0 -33
- package/template/.agents/skills/apps/skybridge/references/deploy.md +0 -33
- package/template/.agents/skills/apps/skybridge/references/discover.md +0 -84
- package/template/.agents/skills/apps/skybridge/references/download-file.md +0 -77
- package/template/.agents/skills/apps/skybridge/references/fetch-and-render-data.md +0 -151
- package/template/.agents/skills/apps/skybridge/references/oauth.md +0 -115
- package/template/.agents/skills/apps/skybridge/references/open-external-links.md +0 -71
- package/template/.agents/skills/apps/skybridge/references/prompt-llm.md +0 -20
- package/template/.agents/skills/apps/skybridge/references/publish.md +0 -19
- package/template/.agents/skills/apps/skybridge/references/run-locally.md +0 -51
- package/template/.agents/skills/apps/skybridge/references/state-and-context.md +0 -151
- package/template/.agents/skills/apps/skybridge/references/ui-guidelines.md +0 -205
- package/template/.agents/skills/design/impeccable/scripts/cleanup-deprecated.mjs +0 -284
- package/template/.agents/skills/design/impeccable/scripts/impeccable-paths.mjs +0 -126
- /package/template/.agents/skills/design/impeccable/scripts/{is-generated.mjs → lib/is-generated.mjs} +0 -0
- /package/template/.agents/skills/design/impeccable/scripts/{live-completion.mjs → live/completion.mjs} +0 -0
- /package/template/.agents/skills/design/impeccable/scripts/{live-insert-ui.mjs → live/insert-ui.mjs} +0 -0
- /package/template/.agents/skills/design/impeccable/scripts/{live-svelte-component.mjs → live/svelte-component.mjs} +0 -0
- /package/template/.agents/skills/design/impeccable/scripts/{live-sveltekit-adapter.mjs → live/sveltekit-adapter.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,26 +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';
|
|
36
45
|
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';
|
|
46
|
+
createManualApplyController,
|
|
47
|
+
summarizeManualApplyFailures,
|
|
48
|
+
} from './live/manual-apply.mjs';
|
|
45
49
|
import {
|
|
46
50
|
applyDeferredSvelteComponentAccepts,
|
|
47
51
|
removeAllSvelteComponentSessions,
|
|
48
|
-
} from './live
|
|
52
|
+
} from './live/svelte-component.mjs';
|
|
49
53
|
|
|
50
54
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
51
55
|
// PRODUCT.md / DESIGN.md live wherever context.mjs resolves. The generated
|
|
52
56
|
// DESIGN sidecar is project-local at .impeccable/design.json, with legacy
|
|
53
57
|
// DESIGN.json fallback for existing projects.
|
|
54
|
-
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;
|
|
55
63
|
const DEFAULT_POLL_TIMEOUT = 600_000; // 10 min — agent re-polls on timeout anyway
|
|
56
64
|
const SSE_HEARTBEAT_INTERVAL = 30_000; // keepalive ping every 30s
|
|
57
65
|
|
|
@@ -100,23 +108,29 @@ const state = {
|
|
|
100
108
|
};
|
|
101
109
|
|
|
102
110
|
const CHAT_POLL_FRESHNESS_MS = 60_000;
|
|
103
|
-
const APPLY_EVENT_HARD_TIMEOUT_MS = Number(process.env.IMPECCABLE_LIVE_APPLY_EVENT_HARD_TIMEOUT_MS || 150_000);
|
|
104
|
-
const APPLY_EVENT_SOFT_DEADLINE_MS = Number(process.env.IMPECCABLE_LIVE_APPLY_EVENT_SOFT_DEADLINE_MS || 120_000);
|
|
105
|
-
const DEFAULT_MANUAL_EDIT_APPLY_CHUNK_SIZE = 3;
|
|
106
|
-
const MIN_MANUAL_EDIT_APPLY_CHUNK_SIZE = 1;
|
|
107
|
-
const MAX_MANUAL_EDIT_APPLY_CHUNK_SIZE = 20;
|
|
108
|
-
const MANUAL_APPLY_COMPACT_TEXT_LIMIT = 240;
|
|
109
|
-
const MANUAL_APPLY_COMPACT_NEARBY_LIMIT = 4;
|
|
110
111
|
const POLL_LEASE_EXPIRY_TIMER_GRACE_MS = 2;
|
|
111
112
|
const DEBUG_MANUAL_EDIT_EVENTS = /^(1|true|yes)$/i.test(process.env.IMPECCABLE_LIVE_DEBUG_EVENTS || '');
|
|
112
113
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
state.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
+
});
|
|
120
134
|
|
|
121
135
|
function chatAgentLikelyActive() {
|
|
122
136
|
if (state.pendingPolls.length > 0) return true;
|
|
@@ -124,752 +138,6 @@ function chatAgentLikelyActive() {
|
|
|
124
138
|
return Date.now() - state.lastPollAt < CHAT_POLL_FRESHNESS_MS;
|
|
125
139
|
}
|
|
126
140
|
|
|
127
|
-
function manualEditApplyChunkSize(env = process.env) {
|
|
128
|
-
const raw = Number(env.IMPECCABLE_LIVE_MANUAL_EDIT_CHUNK_SIZE);
|
|
129
|
-
if (!Number.isFinite(raw)) return DEFAULT_MANUAL_EDIT_APPLY_CHUNK_SIZE;
|
|
130
|
-
const size = Math.trunc(raw);
|
|
131
|
-
return Math.max(MIN_MANUAL_EDIT_APPLY_CHUNK_SIZE, Math.min(MAX_MANUAL_EDIT_APPLY_CHUNK_SIZE, size));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function countManualApplyOps(entriesOrBatch) {
|
|
135
|
-
const entries = Array.isArray(entriesOrBatch)
|
|
136
|
-
? entriesOrBatch
|
|
137
|
-
: Array.isArray(entriesOrBatch?.entries) ? entriesOrBatch.entries : [];
|
|
138
|
-
let count = 0;
|
|
139
|
-
for (const entry of entries) count += Array.isArray(entry.ops) ? entry.ops.length : 0;
|
|
140
|
-
return count;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function pushApplyEventAndWait(batch, pageUrl, chunk = null, repair = null) {
|
|
144
|
-
const eventId = randomUUID().replace(/-/g, '').slice(0, 8);
|
|
145
|
-
const evidencePath = writeManualApplyEvidence(eventId, batch);
|
|
146
|
-
const event = {
|
|
147
|
-
type: 'manual_edit_apply',
|
|
148
|
-
id: eventId,
|
|
149
|
-
pageUrl,
|
|
150
|
-
batch: compactManualApplyBatch(batch),
|
|
151
|
-
evidencePath,
|
|
152
|
-
agentAction: buildManualApplyAgentAction(eventId),
|
|
153
|
-
schemaVersion: 1,
|
|
154
|
-
deadlineMs: APPLY_EVENT_SOFT_DEADLINE_MS,
|
|
155
|
-
};
|
|
156
|
-
if (chunk) event.chunk = chunk;
|
|
157
|
-
if (repair) event.repair = repair;
|
|
158
|
-
const rollbackSnapshot = snapshotApplyEventFiles(batch);
|
|
159
|
-
recordManualEditActivity('manual_edit_apply_dispatched', {
|
|
160
|
-
id: eventId,
|
|
161
|
-
pageUrl,
|
|
162
|
-
chunk,
|
|
163
|
-
repair,
|
|
164
|
-
entryCount: Array.isArray(batch.entries) ? batch.entries.length : 0,
|
|
165
|
-
opCount: countManualApplyOps(batch),
|
|
166
|
-
fileCount: collectManualApplyFiles(batch).length,
|
|
167
|
-
});
|
|
168
|
-
return new Promise((resolve, reject) => {
|
|
169
|
-
const timer = setTimeout(() => {
|
|
170
|
-
state.pendingApplyDeferreds.delete(eventId);
|
|
171
|
-
tombstoneTimedOutApplyId(eventId, { batch, rollbackSnapshot });
|
|
172
|
-
acknowledgePendingEvent(eventId);
|
|
173
|
-
removeManualApplyEvidence(evidencePath);
|
|
174
|
-
recordManualEditActivity('manual_edit_apply_timeout', {
|
|
175
|
-
id: eventId,
|
|
176
|
-
pageUrl,
|
|
177
|
-
chunk,
|
|
178
|
-
entryCount: Array.isArray(batch.entries) ? batch.entries.length : 0,
|
|
179
|
-
opCount: countManualApplyOps(batch),
|
|
180
|
-
});
|
|
181
|
-
reject(new Error('chat_agent_timeout'));
|
|
182
|
-
}, APPLY_EVENT_HARD_TIMEOUT_MS);
|
|
183
|
-
state.pendingApplyDeferreds.set(eventId, { resolve, reject, timer, event, batch, pageUrl, rollbackSnapshot });
|
|
184
|
-
enqueueEvent(event);
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function writeManualApplyEvidence(eventId, batch) {
|
|
189
|
-
const dir = manualApplyEvidenceDir(process.cwd());
|
|
190
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
191
|
-
const evidencePath = path.join(dir, `${eventId}.json`);
|
|
192
|
-
fs.writeFileSync(evidencePath, JSON.stringify(batch, null, 2) + '\n', 'utf-8');
|
|
193
|
-
return evidencePath;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function manualApplyEvidenceDir(cwd = process.cwd()) {
|
|
197
|
-
return path.join(getLiveDir(cwd), 'manual-edit-evidence');
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function normalizeManualApplyEvidencePath(evidencePath, cwd = process.cwd()) {
|
|
201
|
-
if (!evidencePath || typeof evidencePath !== 'string') return null;
|
|
202
|
-
const fullPath = path.isAbsolute(evidencePath) ? evidencePath : path.resolve(cwd, evidencePath);
|
|
203
|
-
const evidenceDir = manualApplyEvidenceDir(cwd);
|
|
204
|
-
const relative = path.relative(evidenceDir, fullPath);
|
|
205
|
-
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;
|
|
206
|
-
if (path.extname(relative) !== '.json') return null;
|
|
207
|
-
return fullPath;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function removeManualApplyEvidence(evidencePath, cwd = process.cwd()) {
|
|
211
|
-
const fullPath = normalizeManualApplyEvidencePath(evidencePath, cwd);
|
|
212
|
-
if (!fullPath) return false;
|
|
213
|
-
try {
|
|
214
|
-
fs.unlinkSync(fullPath);
|
|
215
|
-
return true;
|
|
216
|
-
} catch {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function referencedManualApplyEvidencePaths(cwd = process.cwd()) {
|
|
222
|
-
const referenced = new Set();
|
|
223
|
-
const add = (event) => {
|
|
224
|
-
const fullPath = normalizeManualApplyEvidencePath(event?.evidencePath, cwd);
|
|
225
|
-
if (fullPath) referenced.add(fullPath);
|
|
226
|
-
};
|
|
227
|
-
for (const entry of state.pendingEvents) add(entry.event);
|
|
228
|
-
for (const deferred of state.pendingApplyDeferreds.values()) add(deferred.event);
|
|
229
|
-
return referenced;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function pruneStaleManualApplyEvidence(cwd = process.cwd()) {
|
|
233
|
-
const dir = manualApplyEvidenceDir(cwd);
|
|
234
|
-
if (!fs.existsSync(dir)) return [];
|
|
235
|
-
const referenced = referencedManualApplyEvidencePaths(cwd);
|
|
236
|
-
const removed = [];
|
|
237
|
-
for (const name of fs.readdirSync(dir)) {
|
|
238
|
-
if (!name.endsWith('.json')) continue;
|
|
239
|
-
const fullPath = path.join(dir, name);
|
|
240
|
-
if (referenced.has(fullPath)) continue;
|
|
241
|
-
try {
|
|
242
|
-
fs.unlinkSync(fullPath);
|
|
243
|
-
removed.push(fullPath);
|
|
244
|
-
} catch {
|
|
245
|
-
// Stale evidence cleanup is best-effort; Apply verification never relies
|
|
246
|
-
// on deleting these files.
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
return removed;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function compactManualApplyBatch(batch = {}) {
|
|
253
|
-
const entries = (batch.entries || []).map(compactManualApplyEntry);
|
|
254
|
-
const candidates = compactManualApplyCandidates(batch.candidates || []);
|
|
255
|
-
return {
|
|
256
|
-
version: batch.version,
|
|
257
|
-
pageUrl: batch.pageUrl || null,
|
|
258
|
-
count: batch.count,
|
|
259
|
-
entries,
|
|
260
|
-
ops: entries.flatMap((entry) => entry.ops.map((op) => ({ ...op, entryId: entry.id }))),
|
|
261
|
-
candidates: candidates.length > 0 ? candidates : undefined,
|
|
262
|
-
context: batch.context ? {
|
|
263
|
-
bufferPath: batch.context.bufferPath,
|
|
264
|
-
totalEntries: batch.context.totalEntries,
|
|
265
|
-
totalOps: batch.context.totalOps,
|
|
266
|
-
chunkIndex: batch.context.chunkIndex,
|
|
267
|
-
chunkTotal: batch.context.chunkTotal,
|
|
268
|
-
totalApplyOps: batch.context.totalApplyOps,
|
|
269
|
-
} : undefined,
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function compactManualApplyCandidates(candidates) {
|
|
274
|
-
return (Array.isArray(candidates) ? candidates : [])
|
|
275
|
-
.slice(0, 24)
|
|
276
|
-
.map((candidate) => ({
|
|
277
|
-
entryId: candidate.entryId,
|
|
278
|
-
ref: candidate.ref,
|
|
279
|
-
sourceHint: compactManualApplySourceMatch(candidate.sourceHint),
|
|
280
|
-
textMatches: compactManualApplySourceMatches(candidate.textMatches, 8),
|
|
281
|
-
objectKeyMatches: compactManualApplySourceMatches(candidate.objectKeyMatches, 8),
|
|
282
|
-
contextTextMatches: compactManualApplySourceMatches(candidate.contextTextMatches, 8),
|
|
283
|
-
locatorMatches: compactManualApplySourceMatches(candidate.locatorMatches, 6),
|
|
284
|
-
}));
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function compactManualApplySourceMatches(matches, limit) {
|
|
288
|
-
return (Array.isArray(matches) ? matches : [])
|
|
289
|
-
.slice(0, limit)
|
|
290
|
-
.map(compactManualApplySourceMatch)
|
|
291
|
-
.filter(Boolean);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function compactManualApplySourceMatch(match) {
|
|
295
|
-
if (!match || typeof match !== 'object') return null;
|
|
296
|
-
const file = match.relativeFile || match.file;
|
|
297
|
-
if (!file && !match.line) return null;
|
|
298
|
-
return {
|
|
299
|
-
file: summarizeManualLogFile(file),
|
|
300
|
-
line: match.line || null,
|
|
301
|
-
column: match.column || null,
|
|
302
|
-
reason: match.reason || match.kind || undefined,
|
|
303
|
-
status: match.status || undefined,
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
function compactManualApplyEntry(entry = {}) {
|
|
308
|
-
return {
|
|
309
|
-
id: entry.id,
|
|
310
|
-
pageUrl: entry.pageUrl,
|
|
311
|
-
stagedAt: entry.stagedAt || null,
|
|
312
|
-
element: compactManualApplyContext(entry.element),
|
|
313
|
-
ops: (entry.ops || []).map(compactManualApplyOp),
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
function compactManualApplyOp(op = {}) {
|
|
318
|
-
return {
|
|
319
|
-
entryId: op.entryId,
|
|
320
|
-
ref: op.ref,
|
|
321
|
-
contextRef: op.contextRef,
|
|
322
|
-
tag: op.tag,
|
|
323
|
-
elementId: op.elementId,
|
|
324
|
-
classes: Array.isArray(op.classes) ? op.classes : [],
|
|
325
|
-
originalText: op.originalText,
|
|
326
|
-
newText: op.newText,
|
|
327
|
-
deleted: op.deleted === true || undefined,
|
|
328
|
-
sourceHint: op.sourceHint || null,
|
|
329
|
-
leaf: compactManualApplyContext(op.leaf),
|
|
330
|
-
nearbyEditableTexts: compactNearbyManualEditTexts(op.nearbyEditableTexts),
|
|
331
|
-
container: compactManualApplyContext(op.container),
|
|
332
|
-
contextHints: Array.isArray(op.contextHints) ? op.contextHints.slice(0, 8) : undefined,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
function compactManualApplyContext(value) {
|
|
337
|
-
if (!value || typeof value !== 'object') return null;
|
|
338
|
-
return {
|
|
339
|
-
ref: value.ref,
|
|
340
|
-
tagName: value.tagName || value.tag || null,
|
|
341
|
-
id: value.id || null,
|
|
342
|
-
classes: Array.isArray(value.classes) ? value.classes : [],
|
|
343
|
-
textContent: truncateManualApplyText(value.textContent, MANUAL_APPLY_COMPACT_TEXT_LIMIT),
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function compactNearbyManualEditTexts(items) {
|
|
348
|
-
return (Array.isArray(items) ? items : [])
|
|
349
|
-
.slice(0, MANUAL_APPLY_COMPACT_NEARBY_LIMIT)
|
|
350
|
-
.map((item) => typeof item === 'string' ? { text: truncateManualApplyText(item, MANUAL_APPLY_COMPACT_TEXT_LIMIT) } : {
|
|
351
|
-
ref: item?.ref,
|
|
352
|
-
tag: item?.tag,
|
|
353
|
-
classes: Array.isArray(item?.classes) ? item.classes : [],
|
|
354
|
-
text: truncateManualApplyText(item?.text, MANUAL_APPLY_COMPACT_TEXT_LIMIT),
|
|
355
|
-
});
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function truncateManualApplyText(value, max) {
|
|
359
|
-
if (typeof value !== 'string') return value || null;
|
|
360
|
-
return value.length > max ? value.slice(0, max) : value;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
async function pushApplyBatchInChunksAndWait(batch, pageUrl, context = {}) {
|
|
364
|
-
const repair = context?.repair || batch?.repair || null;
|
|
365
|
-
if (repair) return pushApplyEventAndWait(batch, pageUrl, null, repair);
|
|
366
|
-
const chunks = splitManualApplyBatch(batch, manualEditApplyChunkSize());
|
|
367
|
-
if (chunks.length <= 1) return pushApplyEventAndWait(batch, pageUrl);
|
|
368
|
-
|
|
369
|
-
const expectedOpsByEntry = new Map();
|
|
370
|
-
for (const entry of batch?.entries || []) {
|
|
371
|
-
expectedOpsByEntry.set(entry.id, Array.isArray(entry.ops) ? entry.ops.length : 0);
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const appliedOpsByEntry = new Map();
|
|
375
|
-
const failedByEntry = new Map();
|
|
376
|
-
const files = new Set();
|
|
377
|
-
const notes = [];
|
|
378
|
-
let aborted = false;
|
|
379
|
-
|
|
380
|
-
for (const chunk of chunks) {
|
|
381
|
-
if (aborted) {
|
|
382
|
-
markChunkEntriesFailed(failedByEntry, chunk, 'manual_edit_chunk_aborted');
|
|
383
|
-
continue;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
let result;
|
|
387
|
-
try {
|
|
388
|
-
result = normalizeApplyChunkResult(await pushApplyEventAndWait(chunk.batch, pageUrl, chunk.meta));
|
|
389
|
-
} catch (err) {
|
|
390
|
-
markChunkEntriesFailed(failedByEntry, chunk, err.message || 'chat_agent_error');
|
|
391
|
-
aborted = true;
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
for (const file of result.files) files.add(file);
|
|
396
|
-
notes.push(...result.notes);
|
|
397
|
-
|
|
398
|
-
const chunkFailedIds = new Set();
|
|
399
|
-
for (const item of result.failed) {
|
|
400
|
-
const entryId = item.entryId || item.id;
|
|
401
|
-
if (!entryId) continue;
|
|
402
|
-
chunkFailedIds.add(entryId);
|
|
403
|
-
if (!failedByEntry.has(entryId)) {
|
|
404
|
-
failedByEntry.set(entryId, {
|
|
405
|
-
entryId,
|
|
406
|
-
reason: item.reason || item.message || 'failed',
|
|
407
|
-
candidates: Array.isArray(item.candidates) ? item.candidates : [],
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
if (result.status === 'error') {
|
|
413
|
-
markChunkEntriesFailed(failedByEntry, chunk, result.message || firstFailureReason(result) || 'chat_agent_error');
|
|
414
|
-
aborted = true;
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
const reportedAppliedIds = new Set(result.appliedEntryIds);
|
|
419
|
-
for (const entryId of reportedAppliedIds) {
|
|
420
|
-
if (!chunk.entryIds.has(entryId) || chunkFailedIds.has(entryId)) continue;
|
|
421
|
-
appliedOpsByEntry.set(entryId, (appliedOpsByEntry.get(entryId) || 0) + (chunk.opCountsByEntry.get(entryId) || 0));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
for (const entryId of chunk.entryIds) {
|
|
425
|
-
if (reportedAppliedIds.has(entryId) || chunkFailedIds.has(entryId)) continue;
|
|
426
|
-
if (!failedByEntry.has(entryId)) {
|
|
427
|
-
failedByEntry.set(entryId, { entryId, reason: 'not_reported_applied', candidates: [] });
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
const appliedEntryIds = [];
|
|
433
|
-
for (const [entryId, expectedOps] of expectedOpsByEntry.entries()) {
|
|
434
|
-
if (failedByEntry.has(entryId)) continue;
|
|
435
|
-
if ((appliedOpsByEntry.get(entryId) || 0) === expectedOps && expectedOps > 0) {
|
|
436
|
-
appliedEntryIds.push(entryId);
|
|
437
|
-
} else if (!failedByEntry.has(entryId)) {
|
|
438
|
-
failedByEntry.set(entryId, { entryId, reason: 'not_reported_applied', candidates: [] });
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
const failed = [...failedByEntry.values()];
|
|
443
|
-
return {
|
|
444
|
-
status: failed.length === 0 ? 'done' : appliedEntryIds.length > 0 ? 'partial' : 'error',
|
|
445
|
-
appliedEntryIds,
|
|
446
|
-
failed,
|
|
447
|
-
files: [...files],
|
|
448
|
-
notes,
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function normalizeApplyChunkResult(result) {
|
|
453
|
-
const status = result?.status === 'partial' ? 'partial' : result?.status === 'error' ? 'error' : 'done';
|
|
454
|
-
return {
|
|
455
|
-
status,
|
|
456
|
-
message: typeof result?.message === 'string' ? result.message : null,
|
|
457
|
-
appliedEntryIds: Array.isArray(result?.appliedEntryIds) ? result.appliedEntryIds.filter((id) => typeof id === 'string') : [],
|
|
458
|
-
failed: Array.isArray(result?.failed) ? result.failed.filter(Boolean) : [],
|
|
459
|
-
files: Array.isArray(result?.files) ? result.files.filter((file) => typeof file === 'string') : [],
|
|
460
|
-
notes: Array.isArray(result?.notes) ? result.notes.filter((note) => typeof note === 'string') : [],
|
|
461
|
-
};
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
function manualApplyResultShapeHint(eventId = 'EVENT_ID') {
|
|
465
|
-
return `Use live-poll.mjs --reply ${eventId} done --data '{"status":"done","appliedEntryIds":["ENTRY_ID"],"failed":[],"files":["src/page.html"],"notes":[]}'`;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
function invalidManualApplyResult(reason, eventId, extra = {}) {
|
|
469
|
-
return {
|
|
470
|
-
ok: false,
|
|
471
|
-
body: {
|
|
472
|
-
error: 'invalid_manual_apply_result',
|
|
473
|
-
reason,
|
|
474
|
-
hint: manualApplyResultShapeHint(eventId),
|
|
475
|
-
...extra,
|
|
476
|
-
},
|
|
477
|
-
};
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
function validateManualApplyResultMessage(msg, deferred) {
|
|
481
|
-
let data = msg?.data;
|
|
482
|
-
const eventId = msg?.id || deferred?.event?.id || 'EVENT_ID';
|
|
483
|
-
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
484
|
-
return invalidManualApplyResult('missing_result_data', eventId);
|
|
485
|
-
}
|
|
486
|
-
if ('entries' in data || 'ops' in data) {
|
|
487
|
-
return invalidManualApplyResult('summary_result_not_allowed', eventId);
|
|
488
|
-
}
|
|
489
|
-
if (!['done', 'partial', 'error'].includes(data.status)) {
|
|
490
|
-
return invalidManualApplyResult('invalid_status', eventId, { status: data.status ?? null });
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
for (const key of ['appliedEntryIds', 'failed', 'files', 'notes']) {
|
|
494
|
-
if (!Array.isArray(data[key])) {
|
|
495
|
-
return invalidManualApplyResult(`${key}_must_be_array`, eventId);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
for (const [index, value] of data.appliedEntryIds.entries()) {
|
|
500
|
-
if (typeof value !== 'string' || !value) {
|
|
501
|
-
return invalidManualApplyResult('appliedEntryIds_must_contain_strings', eventId, { index });
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
for (const [index, value] of data.files.entries()) {
|
|
505
|
-
if (typeof value !== 'string' || !value) {
|
|
506
|
-
return invalidManualApplyResult('files_must_contain_strings', eventId, { index });
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
for (const [index, value] of data.notes.entries()) {
|
|
510
|
-
if (typeof value !== 'string') {
|
|
511
|
-
return invalidManualApplyResult('notes_must_contain_strings', eventId, { index });
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
for (const [index, item] of data.failed.entries()) {
|
|
515
|
-
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
516
|
-
return invalidManualApplyResult('failed_must_contain_objects', eventId, { index });
|
|
517
|
-
}
|
|
518
|
-
if (typeof item.entryId !== 'string' || !item.entryId) {
|
|
519
|
-
return invalidManualApplyResult('failed_entryId_required', eventId, { index });
|
|
520
|
-
}
|
|
521
|
-
if (typeof item.reason !== 'string' || !item.reason) {
|
|
522
|
-
return invalidManualApplyResult('failed_reason_required', eventId, { index });
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
const eventEntryIds = new Set((deferred?.batch?.entries || []).map((entry) => entry.id).filter(Boolean));
|
|
527
|
-
for (const entryId of data.appliedEntryIds) {
|
|
528
|
-
if (eventEntryIds.size > 0 && !eventEntryIds.has(entryId)) {
|
|
529
|
-
return invalidManualApplyResult('applied_entry_id_not_in_event', eventId, { entryId });
|
|
530
|
-
}
|
|
531
|
-
}
|
|
532
|
-
for (const item of data.failed) {
|
|
533
|
-
if (eventEntryIds.size > 0 && !eventEntryIds.has(item.entryId)) {
|
|
534
|
-
return invalidManualApplyResult('failed_entry_id_not_in_event', eventId, { entryId: item.entryId });
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (data.status === 'done') {
|
|
539
|
-
if (data.failed.length > 0) {
|
|
540
|
-
return invalidManualApplyResult('done_result_has_failed_entries', eventId);
|
|
541
|
-
}
|
|
542
|
-
if (countManualApplyOps(deferred?.batch) > 0 && data.appliedEntryIds.length === 0) {
|
|
543
|
-
return invalidManualApplyResult('done_result_missing_applied_entry_ids', eventId);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
if (data.status === 'partial' && data.appliedEntryIds.length === 0 && data.failed.length === 0) {
|
|
547
|
-
return invalidManualApplyResult('partial_result_has_no_entries', eventId);
|
|
548
|
-
}
|
|
549
|
-
if (data.status === 'error' && data.appliedEntryIds.length > 0) {
|
|
550
|
-
return invalidManualApplyResult('error_result_has_applied_entries', eventId);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
return {
|
|
554
|
-
ok: true,
|
|
555
|
-
result: {
|
|
556
|
-
status: data.status,
|
|
557
|
-
message: typeof data.message === 'string' ? data.message : undefined,
|
|
558
|
-
appliedEntryIds: data.appliedEntryIds,
|
|
559
|
-
failed: data.failed,
|
|
560
|
-
files: data.files,
|
|
561
|
-
notes: data.notes,
|
|
562
|
-
},
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function firstFailureReason(result) {
|
|
567
|
-
const first = Array.isArray(result?.failed) ? result.failed.find(Boolean) : null;
|
|
568
|
-
return first?.reason || first?.message || null;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function markChunkEntriesFailed(failedByEntry, chunk, reason) {
|
|
572
|
-
for (const entryId of chunk.entryIds) {
|
|
573
|
-
if (failedByEntry.has(entryId)) continue;
|
|
574
|
-
failedByEntry.set(entryId, { entryId, reason, candidates: [] });
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
function splitManualApplyBatch(batch, maxOps) {
|
|
579
|
-
const totalOpCount = countManualApplyOps(batch);
|
|
580
|
-
if (totalOpCount <= maxOps) {
|
|
581
|
-
return [{
|
|
582
|
-
batch,
|
|
583
|
-
meta: null,
|
|
584
|
-
entryIds: new Set((batch?.entries || []).map((entry) => entry.id).filter(Boolean)),
|
|
585
|
-
opCountsByEntry: new Map((batch?.entries || []).map((entry) => [entry.id, Array.isArray(entry.ops) ? entry.ops.length : 0])),
|
|
586
|
-
}];
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
const rawChunks = [];
|
|
590
|
-
let current = createManualApplyChunkBuilder();
|
|
591
|
-
for (const entry of batch?.entries || []) {
|
|
592
|
-
const ops = entry.ops || [];
|
|
593
|
-
if (ops.length <= maxOps) {
|
|
594
|
-
if (current.opCount > 0 && current.opCount + ops.length > maxOps) {
|
|
595
|
-
rawChunks.push(current);
|
|
596
|
-
current = createManualApplyChunkBuilder();
|
|
597
|
-
}
|
|
598
|
-
for (const op of ops) addOpToManualApplyChunk(current, entry, op);
|
|
599
|
-
continue;
|
|
600
|
-
}
|
|
601
|
-
if (current.opCount > 0) {
|
|
602
|
-
rawChunks.push(current);
|
|
603
|
-
current = createManualApplyChunkBuilder();
|
|
604
|
-
}
|
|
605
|
-
for (const op of ops) {
|
|
606
|
-
if (current.opCount >= maxOps) {
|
|
607
|
-
rawChunks.push(current);
|
|
608
|
-
current = createManualApplyChunkBuilder();
|
|
609
|
-
}
|
|
610
|
-
addOpToManualApplyChunk(current, entry, op);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
if (current.opCount > 0) rawChunks.push(current);
|
|
614
|
-
|
|
615
|
-
return rawChunks.map((chunk, index) => ({
|
|
616
|
-
batch: {
|
|
617
|
-
...batch,
|
|
618
|
-
count: chunk.opCount,
|
|
619
|
-
entries: chunk.entries,
|
|
620
|
-
ops: chunk.ops,
|
|
621
|
-
candidates: filterManualApplyChunkCandidates(batch, chunk.refsByEntry),
|
|
622
|
-
context: {
|
|
623
|
-
...(batch?.context || {}),
|
|
624
|
-
totalEntries: chunk.entries.length,
|
|
625
|
-
totalOps: chunk.opCount,
|
|
626
|
-
chunkIndex: index + 1,
|
|
627
|
-
chunkTotal: rawChunks.length,
|
|
628
|
-
totalApplyOps: totalOpCount,
|
|
629
|
-
},
|
|
630
|
-
},
|
|
631
|
-
meta: {
|
|
632
|
-
index: index + 1,
|
|
633
|
-
total: rawChunks.length,
|
|
634
|
-
opCount: chunk.opCount,
|
|
635
|
-
totalOpCount,
|
|
636
|
-
},
|
|
637
|
-
entryIds: new Set(chunk.entries.map((entry) => entry.id).filter(Boolean)),
|
|
638
|
-
opCountsByEntry: chunk.opCountsByEntry,
|
|
639
|
-
}));
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
function createManualApplyChunkBuilder() {
|
|
643
|
-
return {
|
|
644
|
-
entries: [],
|
|
645
|
-
entryById: new Map(),
|
|
646
|
-
entryIds: new Set(),
|
|
647
|
-
ops: [],
|
|
648
|
-
refsByEntry: new Map(),
|
|
649
|
-
opCountsByEntry: new Map(),
|
|
650
|
-
opCount: 0,
|
|
651
|
-
};
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
function addOpToManualApplyChunk(chunk, entry, op) {
|
|
655
|
-
let chunkEntry = chunk.entryById.get(entry.id);
|
|
656
|
-
if (!chunkEntry) {
|
|
657
|
-
chunkEntry = { ...entry, ops: [] };
|
|
658
|
-
chunk.entryById.set(entry.id, chunkEntry);
|
|
659
|
-
chunk.entryIds.add(entry.id);
|
|
660
|
-
chunk.entries.push(chunkEntry);
|
|
661
|
-
}
|
|
662
|
-
chunkEntry.ops.push(op);
|
|
663
|
-
chunk.ops.push({ ...op, entryId: op.entryId || entry.id });
|
|
664
|
-
if (!chunk.refsByEntry.has(entry.id)) chunk.refsByEntry.set(entry.id, new Set());
|
|
665
|
-
if (op.ref) chunk.refsByEntry.get(entry.id).add(op.ref);
|
|
666
|
-
chunk.opCountsByEntry.set(entry.id, (chunk.opCountsByEntry.get(entry.id) || 0) + 1);
|
|
667
|
-
chunk.opCount += 1;
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function filterManualApplyChunkCandidates(batch, refsByEntry) {
|
|
671
|
-
return (batch?.candidates || []).filter((candidate) => {
|
|
672
|
-
const refs = refsByEntry.get(candidate.entryId);
|
|
673
|
-
if (!refs) return false;
|
|
674
|
-
if (!candidate.ref) return true;
|
|
675
|
-
return refs.has(candidate.ref);
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function resolveApplyDeferred(eventId, body) {
|
|
680
|
-
const deferred = state.pendingApplyDeferreds.get(eventId);
|
|
681
|
-
if (!deferred) return false;
|
|
682
|
-
state.pendingApplyDeferreds.delete(eventId);
|
|
683
|
-
clearTimeout(deferred.timer);
|
|
684
|
-
removeManualApplyEvidence(deferred.event?.evidencePath);
|
|
685
|
-
deferred.resolve(body);
|
|
686
|
-
return true;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
function rejectApplyDeferred(eventId, reason) {
|
|
690
|
-
const deferred = state.pendingApplyDeferreds.get(eventId);
|
|
691
|
-
if (!deferred) return false;
|
|
692
|
-
state.pendingApplyDeferreds.delete(eventId);
|
|
693
|
-
clearTimeout(deferred.timer);
|
|
694
|
-
removeManualApplyEvidence(deferred.event?.evidencePath);
|
|
695
|
-
deferred.reject(new Error(reason || 'chat_agent_error'));
|
|
696
|
-
return true;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
function snapshotApplyEventFiles(batch) {
|
|
700
|
-
const snapshot = new Map();
|
|
701
|
-
for (const relativeFile of collectManualApplyFiles(batch)) {
|
|
702
|
-
const absolute = path.resolve(process.cwd(), relativeFile);
|
|
703
|
-
try {
|
|
704
|
-
snapshot.set(relativeFile, {
|
|
705
|
-
exists: fs.existsSync(absolute),
|
|
706
|
-
content: fs.existsSync(absolute) ? fs.readFileSync(absolute, 'utf-8') : '',
|
|
707
|
-
});
|
|
708
|
-
} catch {
|
|
709
|
-
// If a file cannot be read before dispatch, do not attempt late rollback.
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
return snapshot;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
function manualApplyTransactionPath(cwd = process.cwd()) {
|
|
716
|
-
return path.join(getLiveDir(cwd), 'manual-edit-apply-transaction.json');
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function readManualApplyTransaction(cwd = process.cwd()) {
|
|
720
|
-
const file = manualApplyTransactionPath(cwd);
|
|
721
|
-
if (!fs.existsSync(file)) return null;
|
|
722
|
-
try {
|
|
723
|
-
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
724
|
-
} catch {
|
|
725
|
-
return null;
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function writeManualApplyTransaction({ cwd = process.cwd(), pageUrl = null, batch }) {
|
|
730
|
-
const file = manualApplyTransactionPath(cwd);
|
|
731
|
-
const files = collectManualApplyFiles(batch);
|
|
732
|
-
const transaction = {
|
|
733
|
-
version: 1,
|
|
734
|
-
id: randomUUID().replace(/-/g, '').slice(0, 8),
|
|
735
|
-
createdAt: new Date().toISOString(),
|
|
736
|
-
pageUrl,
|
|
737
|
-
entryIds: (batch?.entries || []).map((entry) => entry.id).filter(Boolean),
|
|
738
|
-
files: files.map((relativeFile) => {
|
|
739
|
-
const absolute = path.resolve(cwd, relativeFile);
|
|
740
|
-
const exists = fs.existsSync(absolute);
|
|
741
|
-
return {
|
|
742
|
-
file: relativeFile,
|
|
743
|
-
exists,
|
|
744
|
-
content: exists ? fs.readFileSync(absolute, 'utf-8') : '',
|
|
745
|
-
};
|
|
746
|
-
}),
|
|
747
|
-
};
|
|
748
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
749
|
-
fs.writeFileSync(`${file}.tmp`, JSON.stringify(transaction, null, 2) + '\n', 'utf-8');
|
|
750
|
-
fs.renameSync(`${file}.tmp`, file);
|
|
751
|
-
return transaction;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function clearManualApplyTransaction(cwd = process.cwd(), transactionId = null) {
|
|
755
|
-
const file = manualApplyTransactionPath(cwd);
|
|
756
|
-
if (!fs.existsSync(file)) return false;
|
|
757
|
-
if (transactionId) {
|
|
758
|
-
const existing = readManualApplyTransaction(cwd);
|
|
759
|
-
if (existing?.id && existing.id !== transactionId) return false;
|
|
760
|
-
}
|
|
761
|
-
try {
|
|
762
|
-
fs.unlinkSync(file);
|
|
763
|
-
return true;
|
|
764
|
-
} catch {
|
|
765
|
-
return false;
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function rollbackManualApplyTransaction({ cwd = process.cwd(), pageUrl = null, reason = 'manual_edit_transaction_rollback' } = {}) {
|
|
770
|
-
const transaction = readManualApplyTransaction(cwd);
|
|
771
|
-
if (!transaction) return null;
|
|
772
|
-
if (pageUrl && transaction.pageUrl && transaction.pageUrl !== pageUrl) return null;
|
|
773
|
-
|
|
774
|
-
let pendingIds = new Set();
|
|
775
|
-
try {
|
|
776
|
-
const buffer = readManualEditsBuffer(cwd);
|
|
777
|
-
pendingIds = new Set((buffer.entries || []).map((entry) => entry.id).filter(Boolean));
|
|
778
|
-
} catch {
|
|
779
|
-
pendingIds = new Set(transaction.entryIds || []);
|
|
780
|
-
}
|
|
781
|
-
const shouldRollback = (transaction.entryIds || []).some((id) => pendingIds.has(id));
|
|
782
|
-
if (!shouldRollback) {
|
|
783
|
-
clearManualApplyTransaction(cwd, transaction.id);
|
|
784
|
-
return { id: transaction.id, reason, rolledBackFiles: [], rollbackFailures: [], skipped: 'entries_not_pending' };
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
const rolledBackFiles = [];
|
|
788
|
-
const rollbackFailures = [];
|
|
789
|
-
for (const item of transaction.files || []) {
|
|
790
|
-
const relativeFile = normalizeProjectFile(item.file);
|
|
791
|
-
if (!relativeFile) continue;
|
|
792
|
-
const absolute = path.resolve(cwd, relativeFile);
|
|
793
|
-
try {
|
|
794
|
-
if (item.exists) {
|
|
795
|
-
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
796
|
-
fs.writeFileSync(absolute, item.content || '', 'utf-8');
|
|
797
|
-
} else if (fs.existsSync(absolute)) {
|
|
798
|
-
fs.rmSync(absolute);
|
|
799
|
-
}
|
|
800
|
-
rolledBackFiles.push(relativeFile);
|
|
801
|
-
} catch (err) {
|
|
802
|
-
rollbackFailures.push({ file: relativeFile, reason: 'restore_failed', message: err.message || String(err) });
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
clearManualApplyTransaction(cwd, transaction.id);
|
|
806
|
-
recordManualEditActivity('manual_edit_transaction_rolled_back', {
|
|
807
|
-
id: transaction.id,
|
|
808
|
-
pageUrl: transaction.pageUrl || null,
|
|
809
|
-
reason,
|
|
810
|
-
entryIds: transaction.entryIds || [],
|
|
811
|
-
rolledBackFiles: rolledBackFiles.map(summarizeManualLogFile).filter(Boolean),
|
|
812
|
-
rollbackFailures: summarizeManualDiagnostics(rollbackFailures),
|
|
813
|
-
});
|
|
814
|
-
return { id: transaction.id, reason, rolledBackFiles, rollbackFailures };
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function collectManualApplyFiles(batch, extraFiles = []) {
|
|
818
|
-
const files = [];
|
|
819
|
-
for (const entry of batch?.entries || []) {
|
|
820
|
-
for (const op of entry.ops || []) files.push(op.sourceHint?.file);
|
|
821
|
-
}
|
|
822
|
-
for (const candidate of batch?.candidates || []) {
|
|
823
|
-
files.push(candidate.sourceHint?.relativeFile, candidate.sourceHint?.file);
|
|
824
|
-
for (const item of candidate.textMatches || []) files.push(item.file);
|
|
825
|
-
for (const item of candidate.objectKeyMatches || []) files.push(item.file);
|
|
826
|
-
for (const item of candidate.locatorMatches || []) files.push(item.file);
|
|
827
|
-
for (const item of candidate.contextTextMatches || []) files.push(item.file);
|
|
828
|
-
}
|
|
829
|
-
files.push(...(extraFiles || []));
|
|
830
|
-
return [...new Set(files)]
|
|
831
|
-
.map((file) => normalizeProjectFile(file))
|
|
832
|
-
.filter(Boolean);
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function normalizeProjectFile(file) {
|
|
836
|
-
if (!file || typeof file !== 'string') return null;
|
|
837
|
-
const absolute = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file);
|
|
838
|
-
const relative = path.relative(process.cwd(), absolute);
|
|
839
|
-
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return null;
|
|
840
|
-
return relative;
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
function rollbackApplySnapshot(batch, rollbackSnapshot, extraFiles = [], reason = 'manual_edit_apply_snapshot_rollback') {
|
|
844
|
-
const scope = collectManualApplyFiles(batch, extraFiles);
|
|
845
|
-
const rolledBackFiles = [];
|
|
846
|
-
const rollbackFailures = [];
|
|
847
|
-
for (const relativeFile of scope) {
|
|
848
|
-
const before = rollbackSnapshot?.get(relativeFile);
|
|
849
|
-
if (!before) continue;
|
|
850
|
-
const absolute = path.resolve(process.cwd(), relativeFile);
|
|
851
|
-
try {
|
|
852
|
-
if (before.exists) {
|
|
853
|
-
fs.mkdirSync(path.dirname(absolute), { recursive: true });
|
|
854
|
-
fs.writeFileSync(absolute, before.content, 'utf-8');
|
|
855
|
-
} else if (fs.existsSync(absolute)) {
|
|
856
|
-
fs.rmSync(absolute);
|
|
857
|
-
}
|
|
858
|
-
rolledBackFiles.push(relativeFile);
|
|
859
|
-
} catch (err) {
|
|
860
|
-
rollbackFailures.push({ file: relativeFile, reason: 'restore_failed', message: err.message || String(err) });
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
return { rolledBackFiles, rollbackFailures };
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function rollbackTimedOutApplyReply(msg) {
|
|
867
|
-
const details = state.timedOutApplyIds.get(msg.id);
|
|
868
|
-
if (!details) return { rolledBackFiles: [], rollbackFailures: [] };
|
|
869
|
-
state.timedOutApplyIds.delete(msg.id);
|
|
870
|
-
return rollbackApplySnapshot(details.batch, details.rollbackSnapshot, msg.data?.files || [], 'stale_manual_edit_apply_reply');
|
|
871
|
-
}
|
|
872
|
-
|
|
873
141
|
// Cap per-annotation upload size. A full 1920×1080 PNG is typically <1 MB;
|
|
874
142
|
// cap at 10 MB to guard against runaway writes from a misbehaving client.
|
|
875
143
|
const MAX_ANNOTATION_BYTES = 10 * 1024 * 1024;
|
|
@@ -924,32 +192,6 @@ function findPendingEventById(id) {
|
|
|
924
192
|
return entry?.event || null;
|
|
925
193
|
}
|
|
926
194
|
|
|
927
|
-
function manualApplyReplyCommand(eventOrId = 'EVENT_ID') {
|
|
928
|
-
const id = typeof eventOrId === 'string' ? eventOrId : eventOrId?.id || 'EVENT_ID';
|
|
929
|
-
return `live-poll.mjs --reply ${id} done --data '<json>'`;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
function buildManualApplyAgentAction(eventOrId = 'EVENT_ID') {
|
|
933
|
-
return {
|
|
934
|
-
kind: 'manual_edit_apply',
|
|
935
|
-
required: 'apply_source_edits_then_reply',
|
|
936
|
-
replyCommand: manualApplyReplyCommand(eventOrId),
|
|
937
|
-
warning: 'Polling only leases this work item; it does not commit source edits.',
|
|
938
|
-
};
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
function summarizeManualApplyEvent(event = {}, batch = event.batch) {
|
|
942
|
-
const entries = Array.isArray(batch?.entries) ? batch.entries : [];
|
|
943
|
-
const opCount = entries.reduce((sum, entry) => sum + (Array.isArray(entry.ops) ? entry.ops.length : 0), 0);
|
|
944
|
-
return {
|
|
945
|
-
pageUrl: event.pageUrl || null,
|
|
946
|
-
chunk: event.chunk || null,
|
|
947
|
-
entryCount: entries.length,
|
|
948
|
-
opCount,
|
|
949
|
-
files: collectManualApplyFiles(batch),
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
|
|
953
195
|
function summarizePendingEventForStatus(entry) {
|
|
954
196
|
const event = entry.event || {};
|
|
955
197
|
const summary = {
|
|
@@ -963,8 +205,8 @@ function summarizePendingEventForStatus(entry) {
|
|
|
963
205
|
summary.chunk = event.chunk || null;
|
|
964
206
|
summary.repair = event.repair || null;
|
|
965
207
|
summary.evidencePath = event.evidencePath || null;
|
|
966
|
-
summary.agentAction = event.agentAction ||
|
|
967
|
-
summary.manualApplySummary =
|
|
208
|
+
summary.agentAction = event.agentAction || manualApply.buildAgentAction(event);
|
|
209
|
+
summary.manualApplySummary = manualApply.summarizeEvent(event, manualApply.getDeferred(event.id)?.batch || event.batch);
|
|
968
210
|
}
|
|
969
211
|
return summary;
|
|
970
212
|
}
|
|
@@ -1005,47 +247,6 @@ function cancelQueuedAnonymousExitEvents() {
|
|
|
1005
247
|
return removed;
|
|
1006
248
|
}
|
|
1007
249
|
|
|
1008
|
-
function cancelPendingManualApplyEvents(pageUrl, reason = 'manual_edit_discarded') {
|
|
1009
|
-
const canceledById = new Map();
|
|
1010
|
-
const shouldCancel = (event) => event?.type === 'manual_edit_apply' && (!pageUrl || event.pageUrl === pageUrl);
|
|
1011
|
-
|
|
1012
|
-
for (let i = state.pendingEvents.length - 1; i >= 0; i -= 1) {
|
|
1013
|
-
const event = state.pendingEvents[i]?.event;
|
|
1014
|
-
if (!shouldCancel(event)) continue;
|
|
1015
|
-
state.pendingEvents.splice(i, 1);
|
|
1016
|
-
removeManualApplyEvidence(event.evidencePath);
|
|
1017
|
-
canceledById.set(event.id, {
|
|
1018
|
-
id: event.id,
|
|
1019
|
-
pageUrl: event.pageUrl,
|
|
1020
|
-
entryCount: event.batch?.entries?.length || 0,
|
|
1021
|
-
});
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
for (const [eventId, deferred] of [...state.pendingApplyDeferreds.entries()]) {
|
|
1025
|
-
if (!shouldCancel(deferred.event)) continue;
|
|
1026
|
-
state.pendingApplyDeferreds.delete(eventId);
|
|
1027
|
-
clearTimeout(deferred.timer);
|
|
1028
|
-
const rollback = rollbackApplySnapshot(deferred.batch, deferred.rollbackSnapshot, [], reason);
|
|
1029
|
-
tombstoneTimedOutApplyId(eventId, {
|
|
1030
|
-
batch: deferred.batch,
|
|
1031
|
-
rollbackSnapshot: deferred.rollbackSnapshot,
|
|
1032
|
-
reason,
|
|
1033
|
-
});
|
|
1034
|
-
removeManualApplyEvidence(deferred.event?.evidencePath);
|
|
1035
|
-
canceledById.set(eventId, {
|
|
1036
|
-
id: eventId,
|
|
1037
|
-
pageUrl: deferred.pageUrl,
|
|
1038
|
-
entryCount: deferred.batch?.entries?.length || 0,
|
|
1039
|
-
rolledBackFiles: rollback.rolledBackFiles,
|
|
1040
|
-
rollbackFailures: rollback.rollbackFailures,
|
|
1041
|
-
});
|
|
1042
|
-
deferred.reject(new Error(reason));
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
if (canceledById.size > 0) flushPendingPolls();
|
|
1046
|
-
return [...canceledById.values()];
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
250
|
function scheduleLeaseFlush() {
|
|
1050
251
|
if (state.leaseTimer) {
|
|
1051
252
|
clearTimeout(state.leaseTimer);
|
|
@@ -1137,61 +338,6 @@ function getManualEditStatus() {
|
|
|
1137
338
|
}
|
|
1138
339
|
}
|
|
1139
340
|
|
|
1140
|
-
function summarizePendingManualEditBatch(pageUrl = null) {
|
|
1141
|
-
try {
|
|
1142
|
-
const buffer = readManualEditsBuffer(process.cwd());
|
|
1143
|
-
const entries = (buffer.entries || [])
|
|
1144
|
-
.filter((entry) => !pageUrl || entry.pageUrl === pageUrl);
|
|
1145
|
-
return {
|
|
1146
|
-
pendingEntryCount: entries.length,
|
|
1147
|
-
pendingOpCount: entries.reduce((sum, entry) => sum + (entry.ops?.length || 0), 0),
|
|
1148
|
-
};
|
|
1149
|
-
} catch (err) {
|
|
1150
|
-
return { pendingSummaryError: err.message || String(err) };
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
function summarizeManualApplyFailures(failed) {
|
|
1155
|
-
if (!Array.isArray(failed)) return [];
|
|
1156
|
-
return failed.slice(0, 20).map((item) => ({
|
|
1157
|
-
id: item.id || item.entryId || null,
|
|
1158
|
-
reason: item.reason || item.message || 'failed',
|
|
1159
|
-
message: compactManualLogText(item.message, 300),
|
|
1160
|
-
files: Array.isArray(item.files) ? item.files.slice(0, 12).map(summarizeManualLogFile).filter(Boolean) : undefined,
|
|
1161
|
-
checks: summarizeManualDiagnostics(item.checks),
|
|
1162
|
-
failures: summarizeManualDiagnostics(item.failures),
|
|
1163
|
-
candidates: summarizeManualDiagnostics(item.candidates),
|
|
1164
|
-
}));
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
function summarizeManualDiagnostics(items) {
|
|
1168
|
-
if (!Array.isArray(items) || items.length === 0) return undefined;
|
|
1169
|
-
return items.slice(0, 12).map((item) => ({
|
|
1170
|
-
reason: item.reason || item.kind || undefined,
|
|
1171
|
-
detail: compactManualLogText(item.detail, 220),
|
|
1172
|
-
message: compactManualLogText(item.message, 300),
|
|
1173
|
-
file: summarizeManualLogFile(item.file || item.relativeFile),
|
|
1174
|
-
line: item.line || undefined,
|
|
1175
|
-
ref: compactManualLogText(item.ref, 180),
|
|
1176
|
-
marker: compactManualLogText(item.marker, 120),
|
|
1177
|
-
files: Array.isArray(item.files) ? item.files.slice(0, 8).map(summarizeManualLogFile).filter(Boolean) : undefined,
|
|
1178
|
-
}));
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
function summarizeManualLogFile(file) {
|
|
1182
|
-
if (!file || typeof file !== 'string') return undefined;
|
|
1183
|
-
if (!path.isAbsolute(file)) return file;
|
|
1184
|
-
const relative = path.relative(process.cwd(), file);
|
|
1185
|
-
return relative && !relative.startsWith('..') && !path.isAbsolute(relative) ? relative : file;
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
function compactManualLogText(value, max = 200) {
|
|
1189
|
-
if (typeof value !== 'string') return undefined;
|
|
1190
|
-
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
1191
|
-
if (normalized.length <= max) return normalized;
|
|
1192
|
-
return normalized.slice(0, max) + `... [truncated ${normalized.length - max} chars]`;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
341
|
// ---------------------------------------------------------------------------
|
|
1196
342
|
// Load scripts
|
|
1197
343
|
// ---------------------------------------------------------------------------
|
|
@@ -1211,29 +357,25 @@ function loadBrowserScripts() {
|
|
|
1211
357
|
try { detectScript = fs.readFileSync(p, 'utf-8'); break; } catch { /* try next */ }
|
|
1212
358
|
}
|
|
1213
359
|
|
|
1214
|
-
//
|
|
1215
|
-
// can re-read on
|
|
1216
|
-
// should land on the next tab reload, not require a server restart.
|
|
1217
|
-
const
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
}
|
|
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);
|
|
1224
369
|
}
|
|
1225
370
|
|
|
1226
|
-
return { detectScript,
|
|
371
|
+
return { detectScript, liveScriptParts };
|
|
1227
372
|
}
|
|
1228
373
|
|
|
1229
374
|
function hasProjectContext() {
|
|
1230
375
|
// PRODUCT.md carries brand voice / anti-references — that's what determines
|
|
1231
376
|
// whether variants are brand-aware. DESIGN.md (visual tokens) is a separate
|
|
1232
377
|
// concern, surfaced by the design panel's own empty state.
|
|
1233
|
-
|
|
1234
|
-
fs.accessSync(path.join(CONTEXT_DIR, 'PRODUCT.md'), fs.constants.R_OK);
|
|
1235
|
-
return true;
|
|
1236
|
-
} catch { return false; }
|
|
378
|
+
return !!PROJECT_CONTEXT.hasProduct;
|
|
1237
379
|
}
|
|
1238
380
|
|
|
1239
381
|
function statOrNull(filePath) {
|
|
@@ -1243,7 +385,7 @@ function statOrNull(filePath) {
|
|
|
1243
385
|
// HTTP request handler
|
|
1244
386
|
// ---------------------------------------------------------------------------
|
|
1245
387
|
|
|
1246
|
-
function createRequestHandler({ detectScript,
|
|
388
|
+
function createRequestHandler({ detectScript, liveScriptParts }) {
|
|
1247
389
|
return (req, res) => {
|
|
1248
390
|
const url = new URL(req.url, `http://localhost:${state.port}`);
|
|
1249
391
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
@@ -1259,21 +401,20 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
|
|
1259
401
|
// the next tab reload. No-store headers prevent browser caching across
|
|
1260
402
|
// sessions — during iteration, a cached old script silently breaks
|
|
1261
403
|
// every subsequent session.
|
|
1262
|
-
let
|
|
1263
|
-
let liveScript;
|
|
404
|
+
let parts;
|
|
1264
405
|
try {
|
|
1265
|
-
|
|
1266
|
-
liveScript = fs.readFileSync(livePath, 'utf-8');
|
|
406
|
+
parts = readLiveBrowserScriptParts(liveScriptParts);
|
|
1267
407
|
} catch (err) {
|
|
1268
408
|
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
1269
409
|
res.end('Error reading live browser scripts: ' + err.message);
|
|
1270
410
|
return;
|
|
1271
411
|
}
|
|
1272
|
-
const body =
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
412
|
+
const body = assembleLiveBrowserScript({
|
|
413
|
+
token: state.token,
|
|
414
|
+
port: state.port,
|
|
415
|
+
vocabulary: LIVE_COMMANDS,
|
|
416
|
+
parts,
|
|
417
|
+
});
|
|
1277
418
|
res.writeHead(200, {
|
|
1278
419
|
'Content-Type': 'application/javascript',
|
|
1279
420
|
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
|
@@ -1409,8 +550,8 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
|
|
1409
550
|
const token = url.searchParams.get('token');
|
|
1410
551
|
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
1411
552
|
|
|
1412
|
-
const mdPath =
|
|
1413
|
-
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());
|
|
1414
555
|
const mdStat = statOrNull(mdPath);
|
|
1415
556
|
const jsonStat = statOrNull(jsonPath);
|
|
1416
557
|
|
|
@@ -1510,334 +651,7 @@ function createRequestHandler({ detectScript, sessionPath, livePath }) {
|
|
|
1510
651
|
return;
|
|
1511
652
|
}
|
|
1512
653
|
|
|
1513
|
-
|
|
1514
|
-
// page batch through the local AI copy-edit runner.
|
|
1515
|
-
if (p === '/manual-edit-stash' && req.method === 'POST') {
|
|
1516
|
-
let body = '';
|
|
1517
|
-
req.on('data', (c) => { body += c; });
|
|
1518
|
-
req.on('end', () => {
|
|
1519
|
-
let msg;
|
|
1520
|
-
try { msg = JSON.parse(body); } catch {
|
|
1521
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1522
|
-
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
1523
|
-
return;
|
|
1524
|
-
}
|
|
1525
|
-
if (msg.token !== state.token) {
|
|
1526
|
-
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
1527
|
-
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
1528
|
-
return;
|
|
1529
|
-
}
|
|
1530
|
-
const error = validateEvent({ ...msg, type: 'manual_edits' });
|
|
1531
|
-
if (error) {
|
|
1532
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1533
|
-
res.end(JSON.stringify({ error }));
|
|
1534
|
-
return;
|
|
1535
|
-
}
|
|
1536
|
-
try {
|
|
1537
|
-
stageManualEditEntry(process.cwd(), {
|
|
1538
|
-
id: msg.id,
|
|
1539
|
-
pageUrl: msg.pageUrl,
|
|
1540
|
-
element: msg.element,
|
|
1541
|
-
ops: msg.ops,
|
|
1542
|
-
});
|
|
1543
|
-
} catch (err) {
|
|
1544
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1545
|
-
res.end(JSON.stringify({ error: 'stash_write_failed', message: err.message }));
|
|
1546
|
-
return;
|
|
1547
|
-
}
|
|
1548
|
-
const { totalCount, perPage } = countPendingByPage(process.cwd());
|
|
1549
|
-
const pendingCount = perPage[msg.pageUrl] || 0;
|
|
1550
|
-
recordManualEditActivity('manual_edit_stashed', {
|
|
1551
|
-
id: msg.id,
|
|
1552
|
-
pageUrl: msg.pageUrl,
|
|
1553
|
-
opCount: msg.ops.length,
|
|
1554
|
-
pendingCount,
|
|
1555
|
-
totalCount,
|
|
1556
|
-
hintedFileCount: new Set((msg.ops || []).map((op) => summarizeManualLogFile(op.sourceHint?.file)).filter(Boolean)).size,
|
|
1557
|
-
});
|
|
1558
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1559
|
-
res.end(JSON.stringify({ ok: true, pendingCount, totalCount, perPage }));
|
|
1560
|
-
});
|
|
1561
|
-
return;
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
// GET /manual-edit-stash?pageUrl=<url> → { count, totalCount, perPage, entries }
|
|
1565
|
-
if (p === '/manual-edit-stash' && req.method === 'GET') {
|
|
1566
|
-
const token = url.searchParams.get('token');
|
|
1567
|
-
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
1568
|
-
const pageUrl = url.searchParams.get('pageUrl') || '';
|
|
1569
|
-
const { totalCount, perPage } = countPendingByPage(process.cwd());
|
|
1570
|
-
const buffer = readManualEditsBuffer(process.cwd());
|
|
1571
|
-
const entriesForPage = pageUrl ? buffer.entries.filter((e) => e.pageUrl === pageUrl) : buffer.entries;
|
|
1572
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1573
|
-
res.end(JSON.stringify({
|
|
1574
|
-
count: pageUrl ? (perPage[pageUrl] || 0) : totalCount,
|
|
1575
|
-
totalCount,
|
|
1576
|
-
perPage,
|
|
1577
|
-
entries: entriesForPage,
|
|
1578
|
-
}));
|
|
1579
|
-
return;
|
|
1580
|
-
}
|
|
1581
|
-
|
|
1582
|
-
// POST /manual-edit-commit?pageUrl=<url> → ask the AI to apply the staged page batch.
|
|
1583
|
-
if (p === '/manual-edit-commit' && req.method === 'POST') {
|
|
1584
|
-
const token = url.searchParams.get('token');
|
|
1585
|
-
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
1586
|
-
const pageUrl = url.searchParams.get('pageUrl');
|
|
1587
|
-
const asyncMode = /^(1|true|yes)$/i.test(url.searchParams.get('async') || '');
|
|
1588
|
-
const repairOnly = /^(1|true|yes)$/i.test(url.searchParams.get('repair') || '');
|
|
1589
|
-
const existingTransaction = readManualApplyTransaction(process.cwd());
|
|
1590
|
-
if (repairOnly && !existingTransaction) {
|
|
1591
|
-
res.writeHead(409, { 'Content-Type': 'application/json' });
|
|
1592
|
-
res.end(JSON.stringify({ error: 'manual_edit_repair_transaction_missing' }));
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
const recoveredTransaction = repairOnly ? null : rollbackManualApplyTransaction({
|
|
1596
|
-
cwd: process.cwd(),
|
|
1597
|
-
pageUrl,
|
|
1598
|
-
reason: 'manual_edit_commit_recovered_abandoned_transaction',
|
|
1599
|
-
});
|
|
1600
|
-
const before = getManualEditStatus();
|
|
1601
|
-
const pendingCount = pageUrl ? (before.perPage[pageUrl] || 0) : before.totalCount;
|
|
1602
|
-
recordManualEditActivity('manual_edit_commit_started', {
|
|
1603
|
-
pageUrl,
|
|
1604
|
-
repairOnly,
|
|
1605
|
-
pendingCount,
|
|
1606
|
-
totalCount: before.totalCount,
|
|
1607
|
-
recoveredTransaction: recoveredTransaction ? {
|
|
1608
|
-
id: recoveredTransaction.id,
|
|
1609
|
-
reason: recoveredTransaction.reason,
|
|
1610
|
-
skipped: recoveredTransaction.skipped,
|
|
1611
|
-
rolledBackFiles: recoveredTransaction.rolledBackFiles,
|
|
1612
|
-
rollbackFailures: summarizeManualDiagnostics(recoveredTransaction.rollbackFailures),
|
|
1613
|
-
} : null,
|
|
1614
|
-
...summarizePendingManualEditBatch(pageUrl),
|
|
1615
|
-
});
|
|
1616
|
-
if (asyncMode) {
|
|
1617
|
-
res.writeHead(202, { 'Content-Type': 'application/json' });
|
|
1618
|
-
res.end(JSON.stringify({
|
|
1619
|
-
status: 'started',
|
|
1620
|
-
pendingCount,
|
|
1621
|
-
totalCount: before.totalCount,
|
|
1622
|
-
perPage: before.perPage,
|
|
1623
|
-
}));
|
|
1624
|
-
}
|
|
1625
|
-
(async () => {
|
|
1626
|
-
let result;
|
|
1627
|
-
let routedProvider = 'subprocess';
|
|
1628
|
-
let transaction = null;
|
|
1629
|
-
let commitBatch = null;
|
|
1630
|
-
try {
|
|
1631
|
-
if (pendingCount > 0) {
|
|
1632
|
-
const transactionBatch = buildManualEditEvidence({ cwd: process.cwd(), pageUrl });
|
|
1633
|
-
commitBatch = transactionBatch;
|
|
1634
|
-
if (!repairOnly && countManualApplyOps(transactionBatch) > 0) {
|
|
1635
|
-
transaction = writeManualApplyTransaction({
|
|
1636
|
-
cwd: process.cwd(),
|
|
1637
|
-
pageUrl,
|
|
1638
|
-
batch: transactionBatch,
|
|
1639
|
-
});
|
|
1640
|
-
} else if (repairOnly && existingTransaction) {
|
|
1641
|
-
transaction = existingTransaction;
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
const requestedMode = (process.env.IMPECCABLE_LIVE_COPY_AGENT || 'auto').trim().toLowerCase();
|
|
1645
|
-
const useChatRoute = requestedMode === 'chat'
|
|
1646
|
-
|| (requestedMode === 'auto' && chatAgentLikelyActive());
|
|
1647
|
-
if (useChatRoute) {
|
|
1648
|
-
routedProvider = 'chat';
|
|
1649
|
-
const timeoutMs = Number(process.env.IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS || 120000);
|
|
1650
|
-
result = await commitManualEdits({
|
|
1651
|
-
cwd: process.cwd(),
|
|
1652
|
-
pageUrl,
|
|
1653
|
-
provider: 'chat',
|
|
1654
|
-
env: process.env,
|
|
1655
|
-
timeoutMs,
|
|
1656
|
-
chatAvailable: chatAgentLikelyActive,
|
|
1657
|
-
applyBatchToSource: (batch, context) => pushApplyBatchInChunksAndWait(batch, pageUrl, context),
|
|
1658
|
-
repairOnly,
|
|
1659
|
-
transactionId: transaction?.id || existingTransaction?.id || null,
|
|
1660
|
-
batch: commitBatch,
|
|
1661
|
-
});
|
|
1662
|
-
} else {
|
|
1663
|
-
const timeoutMs = Number(process.env.IMPECCABLE_LIVE_COPY_AGENT_TIMEOUT_MS || 120000);
|
|
1664
|
-
const provider = ['codex', 'claude', 'mock'].includes(requestedMode) ? requestedMode : undefined;
|
|
1665
|
-
result = await commitManualEdits({
|
|
1666
|
-
cwd: process.cwd(),
|
|
1667
|
-
pageUrl,
|
|
1668
|
-
provider,
|
|
1669
|
-
env: process.env,
|
|
1670
|
-
timeoutMs,
|
|
1671
|
-
chatAvailable: chatAgentLikelyActive,
|
|
1672
|
-
repairOnly,
|
|
1673
|
-
transactionId: transaction?.id || existingTransaction?.id || null,
|
|
1674
|
-
batch: commitBatch,
|
|
1675
|
-
});
|
|
1676
|
-
}
|
|
1677
|
-
} catch (err) {
|
|
1678
|
-
if (transaction) {
|
|
1679
|
-
rollbackManualApplyTransaction({
|
|
1680
|
-
cwd: process.cwd(),
|
|
1681
|
-
pageUrl,
|
|
1682
|
-
reason: 'manual_edit_commit_exception',
|
|
1683
|
-
});
|
|
1684
|
-
}
|
|
1685
|
-
const message = err.stderr?.toString?.() || err.message;
|
|
1686
|
-
recordManualEditActivity('manual_edit_commit_failed', {
|
|
1687
|
-
pageUrl,
|
|
1688
|
-
provider: routedProvider,
|
|
1689
|
-
error: 'manual_edit_commit_failed',
|
|
1690
|
-
message,
|
|
1691
|
-
transactionId: transaction?.id || null,
|
|
1692
|
-
});
|
|
1693
|
-
if (!asyncMode) {
|
|
1694
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1695
|
-
res.end(JSON.stringify({
|
|
1696
|
-
error: 'manual_edit_commit_failed',
|
|
1697
|
-
message,
|
|
1698
|
-
}));
|
|
1699
|
-
}
|
|
1700
|
-
return;
|
|
1701
|
-
} finally {
|
|
1702
|
-
if (transaction) {
|
|
1703
|
-
const shouldKeepTransaction = result?.needsManualDecision === true;
|
|
1704
|
-
if (!shouldKeepTransaction) clearManualApplyTransaction(process.cwd(), transaction.id);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
const { totalCount, perPage } = countPendingByPage(process.cwd());
|
|
1708
|
-
if (result?.needsManualDecision) {
|
|
1709
|
-
recordManualEditActivity('manual_edit_repair_needs_decision', {
|
|
1710
|
-
pageUrl,
|
|
1711
|
-
provider: routedProvider,
|
|
1712
|
-
transactionId: transaction?.id || existingTransaction?.id || null,
|
|
1713
|
-
repair: result.repair || null,
|
|
1714
|
-
failed: summarizeManualApplyFailures(result.failed),
|
|
1715
|
-
files: Array.isArray(result.files) ? result.files.slice(0, 20).map(summarizeManualLogFile).filter(Boolean) : [],
|
|
1716
|
-
remainingCount: pageUrl ? (perPage[pageUrl] || 0) : totalCount,
|
|
1717
|
-
totalCount,
|
|
1718
|
-
});
|
|
1719
|
-
} else {
|
|
1720
|
-
recordManualEditActivity('manual_edit_commit_done', {
|
|
1721
|
-
pageUrl,
|
|
1722
|
-
provider: routedProvider,
|
|
1723
|
-
reason: result.reason || null,
|
|
1724
|
-
repair: result.repair || null,
|
|
1725
|
-
appliedCount: Array.isArray(result.applied) ? result.applied.length : 0,
|
|
1726
|
-
failedCount: Array.isArray(result.failed) ? result.failed.length : 0,
|
|
1727
|
-
failed: summarizeManualApplyFailures(result.failed),
|
|
1728
|
-
files: Array.isArray(result.files) ? result.files.slice(0, 20).map(summarizeManualLogFile).filter(Boolean) : [],
|
|
1729
|
-
warnings: summarizeManualDiagnostics(result.warnings),
|
|
1730
|
-
rolledBackFiles: Array.isArray(result.rolledBackFiles) ? result.rolledBackFiles.slice(0, 20).map(summarizeManualLogFile).filter(Boolean) : [],
|
|
1731
|
-
rollbackFailures: summarizeManualDiagnostics(result.rollbackFailures),
|
|
1732
|
-
unreportedFiles: Array.isArray(result.unreportedFiles) ? result.unreportedFiles.slice(0, 20).map(summarizeManualLogFile).filter(Boolean) : undefined,
|
|
1733
|
-
noteCount: Array.isArray(result.notes) ? result.notes.length : 0,
|
|
1734
|
-
cleared: result.cleared || 0,
|
|
1735
|
-
remainingCount: pageUrl ? (perPage[pageUrl] || 0) : totalCount,
|
|
1736
|
-
totalCount,
|
|
1737
|
-
});
|
|
1738
|
-
}
|
|
1739
|
-
if (!asyncMode) {
|
|
1740
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1741
|
-
res.end(JSON.stringify({ ...result, totalCount, perPage }));
|
|
1742
|
-
}
|
|
1743
|
-
})();
|
|
1744
|
-
return;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
// POST /manual-edit-repair-decision → user resolves an exhausted repair loop.
|
|
1748
|
-
if (p === '/manual-edit-repair-decision' && req.method === 'POST') {
|
|
1749
|
-
let body = '';
|
|
1750
|
-
req.on('data', (chunk) => { body += chunk; });
|
|
1751
|
-
req.on('end', () => {
|
|
1752
|
-
let payload = {};
|
|
1753
|
-
try { payload = body ? JSON.parse(body) : {}; } catch {
|
|
1754
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1755
|
-
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
const token = payload.token || url.searchParams.get('token');
|
|
1759
|
-
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
1760
|
-
const pageUrl = payload.pageUrl || url.searchParams.get('pageUrl') || null;
|
|
1761
|
-
const action = String(payload.action || url.searchParams.get('action') || '').trim().toLowerCase();
|
|
1762
|
-
if (action !== 'rollback') {
|
|
1763
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
1764
|
-
res.end(JSON.stringify({ error: 'unsupported_manual_edit_repair_decision', action }));
|
|
1765
|
-
return;
|
|
1766
|
-
}
|
|
1767
|
-
const rollback = rollbackManualApplyTransaction({
|
|
1768
|
-
cwd: process.cwd(),
|
|
1769
|
-
pageUrl,
|
|
1770
|
-
reason: 'manual_edit_user_requested_rollback',
|
|
1771
|
-
});
|
|
1772
|
-
const { totalCount, perPage } = countPendingByPage(process.cwd());
|
|
1773
|
-
const response = {
|
|
1774
|
-
action,
|
|
1775
|
-
pageUrl,
|
|
1776
|
-
rollback,
|
|
1777
|
-
remainingCount: pageUrl ? (perPage[pageUrl] || 0) : totalCount,
|
|
1778
|
-
totalCount,
|
|
1779
|
-
perPage,
|
|
1780
|
-
};
|
|
1781
|
-
recordManualEditActivity('manual_edit_repair_rollback_done', response);
|
|
1782
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1783
|
-
res.end(JSON.stringify(response));
|
|
1784
|
-
});
|
|
1785
|
-
return;
|
|
1786
|
-
}
|
|
1787
|
-
|
|
1788
|
-
// POST /manual-edit-discard?pageUrl=<url> → drops entries (all if no pageUrl)
|
|
1789
|
-
if (p === '/manual-edit-discard' && req.method === 'POST') {
|
|
1790
|
-
const token = url.searchParams.get('token');
|
|
1791
|
-
if (token !== state.token) { res.writeHead(401); res.end('Unauthorized'); return; }
|
|
1792
|
-
const pageUrl = url.searchParams.get('pageUrl');
|
|
1793
|
-
let discarded;
|
|
1794
|
-
let discardedEntries = [];
|
|
1795
|
-
let canceledApplyEvents = [];
|
|
1796
|
-
let transactionRollback = null;
|
|
1797
|
-
try {
|
|
1798
|
-
const buffer = readManualEditsBuffer(process.cwd());
|
|
1799
|
-
transactionRollback = rollbackManualApplyTransaction({
|
|
1800
|
-
cwd: process.cwd(),
|
|
1801
|
-
pageUrl,
|
|
1802
|
-
reason: 'manual_edit_discarded',
|
|
1803
|
-
});
|
|
1804
|
-
if (pageUrl) {
|
|
1805
|
-
discardedEntries = buffer.entries.filter((entry) => entry.pageUrl === pageUrl);
|
|
1806
|
-
discarded = removeManualEditEntries(process.cwd(), (entry) => entry.pageUrl === pageUrl);
|
|
1807
|
-
} else {
|
|
1808
|
-
discardedEntries = buffer.entries;
|
|
1809
|
-
discarded = truncateManualEditsBuffer(process.cwd());
|
|
1810
|
-
}
|
|
1811
|
-
canceledApplyEvents = cancelPendingManualApplyEvents(pageUrl);
|
|
1812
|
-
} catch (err) {
|
|
1813
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
1814
|
-
res.end(JSON.stringify({ error: 'discard_failed', message: err.message }));
|
|
1815
|
-
return;
|
|
1816
|
-
}
|
|
1817
|
-
const { totalCount, perPage } = countPendingByPage(process.cwd());
|
|
1818
|
-
recordManualEditActivity('manual_edit_discarded', {
|
|
1819
|
-
pageUrl,
|
|
1820
|
-
discarded,
|
|
1821
|
-
canceledApplyIds: canceledApplyEvents.map((event) => event.id),
|
|
1822
|
-
transactionRollback: transactionRollback ? {
|
|
1823
|
-
id: transactionRollback.id,
|
|
1824
|
-
rolledBackFiles: transactionRollback.rolledBackFiles?.map(summarizeManualLogFile).filter(Boolean) || [],
|
|
1825
|
-
rollbackFailures: summarizeManualDiagnostics(transactionRollback.rollbackFailures),
|
|
1826
|
-
skipped: transactionRollback.skipped,
|
|
1827
|
-
} : undefined,
|
|
1828
|
-
totalCount,
|
|
1829
|
-
});
|
|
1830
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1831
|
-
res.end(JSON.stringify({ discarded, entries: discardedEntries, canceledApplyEvents, totalCount, perPage }));
|
|
1832
|
-
return;
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
// Defense in depth: redirect any stragglers from the old /manual-edit endpoint.
|
|
1836
|
-
if (p === '/manual-edit' && req.method === 'POST') {
|
|
1837
|
-
res.writeHead(410, { 'Content-Type': 'application/json' });
|
|
1838
|
-
res.end(JSON.stringify({ error: '/manual-edit is removed; use /manual-edit-stash and /manual-edit-commit for staged copy edits.' }));
|
|
1839
|
-
return;
|
|
1840
|
-
}
|
|
654
|
+
if (manualEditRoutes(req, res, url)) return;
|
|
1841
655
|
|
|
1842
656
|
// --- Browser→server events (replaces WebSocket messages) ---
|
|
1843
657
|
if (p === '/events' && req.method === 'POST') {
|
|
@@ -2008,9 +822,9 @@ function handlePollPost(req, res) {
|
|
|
2008
822
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
|
2009
823
|
return;
|
|
2010
824
|
}
|
|
2011
|
-
const pendingApplyDeferred =
|
|
825
|
+
const pendingApplyDeferred = manualApply.getDeferred(msg.id);
|
|
2012
826
|
if (pendingApplyDeferred) {
|
|
2013
|
-
const validation =
|
|
827
|
+
const validation = manualApply.validateResultMessage(msg, pendingApplyDeferred);
|
|
2014
828
|
if (!validation.ok) {
|
|
2015
829
|
recordManualEditActivity('manual_edit_apply_reply_invalid', {
|
|
2016
830
|
id: msg.id,
|
|
@@ -2035,15 +849,15 @@ function handlePollPost(req, res) {
|
|
|
2035
849
|
fileCount: validation.result.files.length,
|
|
2036
850
|
noteCount: validation.result.notes.length,
|
|
2037
851
|
});
|
|
2038
|
-
|
|
852
|
+
manualApply.resolveDeferred(msg.id, validation.result);
|
|
2039
853
|
acknowledgePendingEvent(msg.id);
|
|
2040
854
|
flushPendingPolls();
|
|
2041
855
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
2042
856
|
res.end(JSON.stringify({ ok: true }));
|
|
2043
857
|
return;
|
|
2044
858
|
}
|
|
2045
|
-
if (
|
|
2046
|
-
const rollback =
|
|
859
|
+
if (manualApply.hasTimedOutId(msg.id)) {
|
|
860
|
+
const rollback = manualApply.rollbackTimedOutReply(msg);
|
|
2047
861
|
recordManualEditActivity('manual_edit_apply_stale_reply_rejected', {
|
|
2048
862
|
id: msg.id,
|
|
2049
863
|
rolledBackFileCount: rollback.rolledBackFiles?.length || 0,
|
|
@@ -2289,13 +1103,12 @@ if (existingRecord?.info) {
|
|
|
2289
1103
|
|
|
2290
1104
|
state.token = randomUUID();
|
|
2291
1105
|
state.sessionStore = createLiveSessionStore({ cwd: process.cwd() });
|
|
2292
|
-
|
|
2293
|
-
cwd: process.cwd(),
|
|
1106
|
+
manualApply.rollbackTransaction({
|
|
2294
1107
|
reason: 'manual_edit_server_start_recovered_abandoned_transaction',
|
|
2295
1108
|
});
|
|
2296
1109
|
applyLegacyDeferredAcceptsOnStartup();
|
|
2297
1110
|
restorePendingEventsFromStore();
|
|
2298
|
-
|
|
1111
|
+
manualApply.pruneStaleEvidence();
|
|
2299
1112
|
const portArg = args.find(a => a.startsWith('--port='));
|
|
2300
1113
|
state.port = portArg ? parseInt(portArg.split('=')[1], 10) : await findOpenPort();
|
|
2301
1114
|
// Annotation screenshots live in the project root so the agent's Read tool
|
|
@@ -2305,8 +1118,8 @@ const annotRoot = getLiveAnnotationsDir(process.cwd());
|
|
|
2305
1118
|
fs.mkdirSync(annotRoot, { recursive: true });
|
|
2306
1119
|
state.sessionDir = fs.mkdtempSync(path.join(annotRoot, 'session-'));
|
|
2307
1120
|
|
|
2308
|
-
const { detectScript,
|
|
2309
|
-
httpServer = http.createServer(createRequestHandler({ detectScript,
|
|
1121
|
+
const { detectScript, liveScriptParts } = loadBrowserScripts();
|
|
1122
|
+
httpServer = http.createServer(createRequestHandler({ detectScript, liveScriptParts }));
|
|
2310
1123
|
|
|
2311
1124
|
httpServer.listen(state.port, '127.0.0.1', () => {
|
|
2312
1125
|
writeLiveServerInfo(process.cwd(), { pid: process.pid, port: state.port, token: state.token });
|