@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.1
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/README.md +122 -2
- package/bin/codeflow.mjs +8 -0
- package/bin/launcher.mjs +127 -0
- package/dist/remote-host.json +1 -0
- package/licenses/inter.txt +92 -0
- package/licenses/preact.txt +21 -0
- package/licenses/signals-core.txt +21 -0
- package/licenses/signals.txt +21 -0
- package/licenses/typescript-notices.txt +193 -0
- package/licenses/typescript.txt +55 -0
- package/licenses/uqr.txt +22 -0
- package/licenses/xterm-fit.txt +19 -0
- package/licenses/xterm.txt +21 -0
- package/package.json +34 -4
- package/public/assets/browser-7XI52A6V.js +8 -0
- package/public/assets/changes-EYFPASEI.js +1 -0
- package/public/assets/chunk-7H3ZLTKA.js +2 -0
- package/public/assets/chunk-L7TUCIYF.js +1 -0
- package/public/assets/chunk-LLMQG23J.js +163 -0
- package/public/assets/main-KIRM2OEK.js +1 -0
- package/public/assets/settings-QJZVW22Y.js +9 -0
- package/public/assets/styles-615EBB20.css +32 -0
- package/public/assets/terminal-A2FI2NAX.js +62 -0
- package/public/assets.json +1 -0
- package/public/bundle-meta.json +1 -0
- package/public/docs/.nojekyll +0 -0
- package/public/docs/accessibility.html +19 -0
- package/public/docs/acp-agents.html +117 -0
- package/public/docs/agent-containers.html +79 -0
- package/public/docs/api.html +1130 -0
- package/public/docs/app.js +342 -0
- package/public/docs/application-improvements.html +275 -0
- package/public/docs/architecture-explorer.html +19 -0
- package/public/docs/architecture.html +149 -0
- package/public/docs/auto-team.html +142 -0
- package/public/docs/automations.html +109 -0
- package/public/docs/checkpoint-review.html +37 -0
- package/public/docs/code-hosts.html +35 -0
- package/public/docs/code-intelligence.html +35 -0
- package/public/docs/configuration.html +346 -0
- package/public/docs/context-inspection.html +23 -0
- package/public/docs/coordinated-changes.html +28 -0
- package/public/docs/deferred-runs.html +25 -0
- package/public/docs/dependency-discovery.html +19 -0
- package/public/docs/documentation.html +84 -0
- package/public/docs/dynamic-teams-evaluation.html +174 -0
- package/public/docs/dynamic-teams-implementation-plan.html +882 -0
- package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
- package/public/docs/dynamic-teams-plan-review-0.html +204 -0
- package/public/docs/extensions.html +42 -0
- package/public/docs/external-sources.html +96 -0
- package/public/docs/filesystem-policy.html +26 -0
- package/public/docs/filtered-tasks.html +44 -0
- package/public/docs/font-license.txt +92 -0
- package/public/docs/getting-started.html +118 -0
- package/public/docs/github-review-threads.html +26 -0
- package/public/docs/github.html +157 -0
- package/public/docs/help.html +69 -0
- package/public/docs/index.html +77 -0
- package/public/docs/installation.html +74 -0
- package/public/docs/launch-profiles.html +48 -0
- package/public/docs/maintenance.html +60 -0
- package/public/docs/memory-claims.html +31 -0
- package/public/docs/memory-storage.html +58 -0
- package/public/docs/notifications.html +39 -0
- package/public/docs/patterns.html +195 -0
- package/public/docs/performance-results.html +210 -0
- package/public/docs/performance.html +166 -0
- package/public/docs/project-conventions.html +22 -0
- package/public/docs/project-first-navigation-plan.html +587 -0
- package/public/docs/project-teams-implementation-plan.html +772 -0
- package/public/docs/project-teams.html +99 -0
- package/public/docs/provider-compatibility.html +20 -0
- package/public/docs/releasing.html +61 -0
- package/public/docs/remote-workers.html +64 -0
- package/public/docs/review-inbox.html +26 -0
- package/public/docs/run-storage.html +23 -0
- package/public/docs/runs.html +141 -0
- package/public/docs/search.json +1 -0
- package/public/docs/security.html +133 -0
- package/public/docs/shared-projects.html +31 -0
- package/public/docs/ssh-projects.html +60 -0
- package/public/docs/style.css +768 -0
- package/public/docs/task-reruns.html +30 -0
- package/public/docs/team-collaboration.html +379 -0
- package/public/docs/verification.html +22 -0
- package/public/docs/vscode.html +53 -0
- package/public/docs/worker-permission-verification.html +122 -0
- package/public/index.html +17 -0
- package/scripts/check-node.mjs +7 -0
- package/scripts/codeflow.mjs +55 -0
- package/scripts/pty.py +273 -0
- package/src/acp-connections.js +110 -0
- package/src/adapters/acp-presets.js +37 -0
- package/src/adapters/acp-types.js +0 -0
- package/src/adapters/acp.js +328 -0
- package/src/adapters/agy-auth.js +67 -0
- package/src/adapters/agy-isolation.js +223 -0
- package/src/adapters/agy.js +297 -0
- package/src/adapters/claude.js +220 -0
- package/src/adapters/codex-app-server.js +518 -0
- package/src/adapters/codex.js +299 -0
- package/src/adapters/errors.js +57 -0
- package/src/adapters/index.js +91 -0
- package/src/adapters/managed-permissions.js +37 -0
- package/src/adapters/model-discovery.js +100 -0
- package/src/adapters/process.js +418 -0
- package/src/adapters/team-tools.js +102 -0
- package/src/adapters/types.js +0 -0
- package/src/agent-container.js +155 -0
- package/src/approvals.js +121 -0
- package/src/architecture.js +456 -0
- package/src/assets.js +44 -0
- package/src/automation.js +901 -0
- package/src/certificate-renewal.js +75 -0
- package/src/chat-handoff.js +55 -0
- package/src/checkpoint-review.js +72 -0
- package/src/checks.js +303 -0
- package/src/code-host-git.js +102 -0
- package/src/code-hosts-api.js +99 -0
- package/src/code-hosts.js +482 -0
- package/src/code-intelligence-worker.mjs +452 -0
- package/src/code-intelligence.js +141 -0
- package/src/collab/api.js +365 -0
- package/src/collab/checkpoints.js +198 -0
- package/src/collab/connections.js +139 -0
- package/src/collab/host-execution.js +347 -0
- package/src/collab/hub-client.js +210 -0
- package/src/collab/link-api.js +229 -0
- package/src/collab/link.js +595 -0
- package/src/collab/model.js +496 -0
- package/src/collab/runner.js +1014 -0
- package/src/collab/store.js +1452 -0
- package/src/collab/terminals.js +361 -0
- package/src/collaboration-reviews.js +116 -0
- package/src/config.js +48 -0
- package/src/context-budget.js +14 -0
- package/src/context-inspection.js +204 -0
- package/src/context.js +154 -0
- package/src/conventions.js +283 -0
- package/src/coordinated-api.js +247 -0
- package/src/deferred-launch.js +82 -0
- package/src/deferred-runs.js +515 -0
- package/src/dependency-health.js +35 -0
- package/src/diff.js +43 -0
- package/src/docs-help.js +316 -0
- package/src/doctor.js +232 -0
- package/src/engine.js +4366 -0
- package/src/env.js +44 -0
- package/src/experience.js +50 -0
- package/src/extensions.js +144 -0
- package/src/filesystem-access.js +107 -0
- package/src/filesystem-policy.js +238 -0
- package/src/filtered-task-limits.js +93 -0
- package/src/filtered-tasks-api.js +53 -0
- package/src/filtered-tasks.js +576 -0
- package/src/fs.js +57 -0
- package/src/git.js +626 -0
- package/src/github-review-threads.js +181 -0
- package/src/github.js +538 -0
- package/src/http.js +69 -0
- package/src/improve-settings.js +38 -0
- package/src/improve.js +187 -0
- package/src/integrations.js +373 -0
- package/src/integrity.js +259 -0
- package/src/journey-api.js +77 -0
- package/src/journey-replay.js +65 -0
- package/src/journey-store.js +97 -0
- package/src/journeys.js +60 -0
- package/src/json.js +81 -0
- package/src/launch-profiles.js +292 -0
- package/src/learnings.js +80 -0
- package/src/log.js +39 -0
- package/src/maintenance.js +335 -0
- package/src/memory-analysis.js +182 -0
- package/src/memory-claims.js +153 -0
- package/src/memory-sqlite.js +236 -0
- package/src/memory.js +552 -0
- package/src/merge-validation.js +58 -0
- package/src/model-catalog.js +238 -0
- package/src/model-research.js +91 -0
- package/src/notes.js +176 -0
- package/src/notifications.js +413 -0
- package/src/patterns/auto-team-model.js +212 -0
- package/src/patterns/auto-team.js +659 -0
- package/src/patterns/council.js +199 -0
- package/src/patterns/debate.js +188 -0
- package/src/patterns/findings.js +141 -0
- package/src/patterns/index.js +19 -0
- package/src/patterns/loop.js +158 -0
- package/src/patterns/pipeline.js +125 -0
- package/src/patterns/prompts.js +181 -0
- package/src/patterns/race.js +147 -0
- package/src/patterns/schemas.js +40 -0
- package/src/patterns/solo.js +49 -0
- package/src/patterns/supervisor.js +235 -0
- package/src/patterns/task-draft.js +29 -0
- package/src/patterns/tdd.js +231 -0
- package/src/patterns/team-strategies.js +67 -0
- package/src/performance.js +43 -0
- package/src/persistence-status.js +35 -0
- package/src/phone-sessions.js +96 -0
- package/src/preview-overlay.js +383 -0
- package/src/preview.js +219 -0
- package/src/privacy.js +23 -0
- package/src/project-report.js +22 -0
- package/src/project-team-model.js +178 -0
- package/src/project-team-policy.js +42 -0
- package/src/project-team-store.js +213 -0
- package/src/project-teams-api.js +36 -0
- package/src/project-teams.js +551 -0
- package/src/projects.js +286 -0
- package/src/provider-eligibility.js +9 -0
- package/src/readiness.js +98 -0
- package/src/remote-tls.js +66 -0
- package/src/remote-workers/bundles.js +198 -0
- package/src/remote-workers/client.js +332 -0
- package/src/remote-workers/durability.js +37 -0
- package/src/remote-workers/protocol.js +68 -0
- package/src/remote-workers/stdio.js +138 -0
- package/src/remote-workers/worker.js +304 -0
- package/src/remote.js +204 -0
- package/src/repo-config.js +201 -0
- package/src/repo-coordination.js +732 -0
- package/src/repo-tree.js +106 -0
- package/src/retention-policy.js +466 -0
- package/src/review-inbox.js +38 -0
- package/src/routing.js +49 -0
- package/src/run-analysis.js +111 -0
- package/src/run-cold-store.js +231 -0
- package/src/run-environment.js +15 -0
- package/src/run-history.js +91 -0
- package/src/run-log.js +160 -0
- package/src/run-permissions.js +110 -0
- package/src/run-projection.js +75 -0
- package/src/run-recovery.js +53 -0
- package/src/run-status.js +54 -0
- package/src/serve.js +138 -0
- package/src/server.js +2738 -0
- package/src/shared-access.js +103 -0
- package/src/shared-accounts.js +301 -0
- package/src/shared-api.js +239 -0
- package/src/shell.js +346 -0
- package/src/ship.js +344 -0
- package/src/slots.js +74 -0
- package/src/snapshot.js +388 -0
- package/src/source-context.js +17 -0
- package/src/sse-writer.js +70 -0
- package/src/ssh-projects/bootstrap.mjs +95 -0
- package/src/ssh-projects/config.js +45 -0
- package/src/ssh-projects/gateway.js +143 -0
- package/src/ssh-projects/manager.js +320 -0
- package/src/ssh-projects/team-autostart.mjs +93 -0
- package/src/ssh-projects/team-remove.mjs +69 -0
- package/src/ssh-projects/team-status.mjs +39 -0
- package/src/ssh-projects/transport.js +116 -0
- package/src/stats.js +139 -0
- package/src/store.js +544 -0
- package/src/stream.js +91 -0
- package/src/sweeps.js +241 -0
- package/src/tailscale.js +101 -0
- package/src/task-draft.js +23 -0
- package/src/team-evaluation.js +242 -0
- package/src/team-mailbox.js +160 -0
- package/src/team-memory.js +84 -0
- package/src/team-rerun-graph.js +15 -0
- package/src/team-reruns.js +130 -0
- package/src/team-scheduler.js +121 -0
- package/src/team-template-store.js +98 -0
- package/src/team-templates.js +117 -0
- package/src/team-tool-client.mjs +79 -0
- package/src/team-tools.js +176 -0
- package/src/teams.js +0 -0
- package/src/templates.js +62 -0
- package/src/transcript.js +134 -0
- package/src/verification-integrity.js +27 -0
- package/src/verification.js +183 -0
- package/src/watch.js +195 -0
- package/src/work-queue.js +47 -0
- package/src/workflow-control.js +83 -0
- package/src/workspace-api.js +207 -0
- package/src/workspace-changes.js +61 -0
- package/src/workspace.js +146 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
class PersistenceTracker {
|
|
2
|
+
states = /* @__PURE__ */ new Map();
|
|
3
|
+
listeners = /* @__PURE__ */ new Map();
|
|
4
|
+
get(id) {
|
|
5
|
+
return this.states.get(id) ?? { state: "saved", seq: 0 };
|
|
6
|
+
}
|
|
7
|
+
subscribe(id, listener) {
|
|
8
|
+
let listeners = this.listeners.get(id);
|
|
9
|
+
if (!listeners) this.listeners.set(id, listeners = /* @__PURE__ */ new Set());
|
|
10
|
+
listeners.add(listener);
|
|
11
|
+
return () => {
|
|
12
|
+
listeners.delete(listener);
|
|
13
|
+
if (!listeners.size) this.listeners.delete(id);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
set(id, status) {
|
|
17
|
+
if ((this.states.get(id)?.seq ?? 0) > status.seq) return;
|
|
18
|
+
this.states.set(id, status);
|
|
19
|
+
for (const listener of this.listeners.get(id) ?? []) listener(status);
|
|
20
|
+
}
|
|
21
|
+
failure(id, seq, error) {
|
|
22
|
+
this.set(id, { state: "error", seq, error });
|
|
23
|
+
}
|
|
24
|
+
track(id, seq, durable) {
|
|
25
|
+
this.set(id, { state: "saving", seq });
|
|
26
|
+
void durable.then(() => this.set(id, { state: "saved", seq }), (error) => this.failure(id, seq, error.message));
|
|
27
|
+
}
|
|
28
|
+
delete(id) {
|
|
29
|
+
this.states.delete(id);
|
|
30
|
+
this.listeners.delete(id);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
PersistenceTracker
|
|
35
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { CODEFLOW_HOME } from "./config.js";
|
|
5
|
+
const digest = (value) => createHash("sha256").update(value).digest("hex");
|
|
6
|
+
const THIRTY_DAYS = 30 * 864e5;
|
|
7
|
+
class PhoneSessions {
|
|
8
|
+
constructor(file = join(CODEFLOW_HOME, "phone-devices.json")) {
|
|
9
|
+
this.file = file;
|
|
10
|
+
}
|
|
11
|
+
records = [];
|
|
12
|
+
loaded = false;
|
|
13
|
+
loading;
|
|
14
|
+
writes = Promise.resolve();
|
|
15
|
+
load() {
|
|
16
|
+
return this.loading ??= (async () => {
|
|
17
|
+
let raw;
|
|
18
|
+
try {
|
|
19
|
+
raw = JSON.parse(await readFile(this.file, "utf8"));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
if (error.code !== "ENOENT") throw new Error("Phone sessions cannot be read; pairing is disabled", { cause: error });
|
|
22
|
+
}
|
|
23
|
+
if (raw !== void 0) {
|
|
24
|
+
const value = raw;
|
|
25
|
+
if (!value || value.version !== 1 || !Array.isArray(value.devices) || value.devices.length > 128 || value.devices.some((device) => !device || typeof device.id !== "string" || device.id.length > 128 || !/^[a-f0-9]{64}$/.test(device.tokenHash) || !/^[a-f0-9]{64}$/.test(device.generation) || typeof device.label !== "string" || device.label.length > 120 || !Number.isFinite(device.createdAt) || !Number.isFinite(device.expiresAt) || device.expiresAt <= device.createdAt || device.revokedAt !== void 0 && (!Number.isFinite(device.revokedAt) || device.revokedAt < device.createdAt))) throw new Error("Phone session storage is invalid; pairing is disabled");
|
|
26
|
+
this.records = value.devices;
|
|
27
|
+
}
|
|
28
|
+
this.loaded = true;
|
|
29
|
+
})();
|
|
30
|
+
}
|
|
31
|
+
valid(token, generation, now = Date.now()) {
|
|
32
|
+
if (!this.loaded || token.length > 128) return false;
|
|
33
|
+
const tokenHash = digest(token), current = digest(generation);
|
|
34
|
+
return this.records.some((device) => device.tokenHash === tokenHash && device.generation === current && !device.revokedAt && device.expiresAt > now);
|
|
35
|
+
}
|
|
36
|
+
list(generation, now = Date.now()) {
|
|
37
|
+
const current = digest(generation);
|
|
38
|
+
return this.records.filter((device) => device.generation === current && !device.revokedAt && device.expiresAt > now).map(({ tokenHash: _, generation: __, ...device }) => ({ ...device }));
|
|
39
|
+
}
|
|
40
|
+
async pair(label, generation, now = Date.now()) {
|
|
41
|
+
await this.load();
|
|
42
|
+
const token = randomBytes(32).toString("hex");
|
|
43
|
+
const device = { id: randomUUID(), label: label.replace(/[\x00-\x1f]/g, "").slice(0, 120) || "Phone", tokenHash: digest(token), generation: digest(generation), createdAt: now, expiresAt: now + THIRTY_DAYS };
|
|
44
|
+
await this.change((records) => {
|
|
45
|
+
const live = records.filter((record) => !record.revokedAt && record.expiresAt > now && record.generation === device.generation);
|
|
46
|
+
if (live.length >= 32) throw new Error("Too many paired devices; revoke an existing device first");
|
|
47
|
+
return [...live, device];
|
|
48
|
+
});
|
|
49
|
+
const { tokenHash: _, generation: __, ...publicDevice } = device;
|
|
50
|
+
return { token, device: publicDevice };
|
|
51
|
+
}
|
|
52
|
+
async revoke(id, generation) {
|
|
53
|
+
await this.load();
|
|
54
|
+
let changed = false;
|
|
55
|
+
await this.change((records) => records.map((device) => {
|
|
56
|
+
if (device.id !== id || device.generation !== digest(generation) || device.revokedAt) return device;
|
|
57
|
+
changed = true;
|
|
58
|
+
return { ...device, revokedAt: Date.now() };
|
|
59
|
+
}));
|
|
60
|
+
return changed;
|
|
61
|
+
}
|
|
62
|
+
change(update) {
|
|
63
|
+
const write = this.writes.catch(() => void 0).then(async () => {
|
|
64
|
+
const records = update(this.records.map((record) => ({ ...record })));
|
|
65
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
66
|
+
const temp = `${this.file}.${randomUUID()}.tmp`;
|
|
67
|
+
try {
|
|
68
|
+
const handle = await open(temp, "wx", 384);
|
|
69
|
+
try {
|
|
70
|
+
await handle.writeFile(JSON.stringify({ version: 1, devices: records }));
|
|
71
|
+
await handle.sync();
|
|
72
|
+
} finally {
|
|
73
|
+
await handle.close();
|
|
74
|
+
}
|
|
75
|
+
await rename(temp, this.file);
|
|
76
|
+
if (process.platform !== "win32") {
|
|
77
|
+
const directory = await open(dirname(this.file), "r");
|
|
78
|
+
try {
|
|
79
|
+
await directory.sync();
|
|
80
|
+
} finally {
|
|
81
|
+
await directory.close();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} finally {
|
|
85
|
+
await rm(temp, { force: true }).catch(() => {
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
this.records = records;
|
|
89
|
+
});
|
|
90
|
+
this.writes = write;
|
|
91
|
+
return write;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export {
|
|
95
|
+
PhoneSessions
|
|
96
|
+
};
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// code-flow's preview overlay. The preview proxy (src/preview.ts) adds this script to the pages
|
|
2
|
+
// of an app shown in the side panel's Browser. It talks only to code-flow's own page, the frame's
|
|
3
|
+
// parent, with postMessage: it reports console errors and failed requests, lets you pick an
|
|
4
|
+
// element or draw a box to comment on (with a best-effort snapshot), shows the numbered pins, and
|
|
5
|
+
// tries style changes on a picked element (Adjust). It does nothing outside code-flow's frame.
|
|
6
|
+
(() => {
|
|
7
|
+
if (window.top === window || window.__codeflowOverlay) return;
|
|
8
|
+
window.__codeflowOverlay = true;
|
|
9
|
+
const PARENTS = __PARENTS__;
|
|
10
|
+
/** The code-flow origin that said hello: messages go only there. */
|
|
11
|
+
let parent = null;
|
|
12
|
+
const early = [];
|
|
13
|
+
const post = (msg) => {
|
|
14
|
+
if (parent) window.parent.postMessage({ codeflow: 1, ...msg }, parent);
|
|
15
|
+
else if (msg.type === "console" && early.length < 200) early.push(msg);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// ------------------------------------------------------------ console and network
|
|
19
|
+
const show = (a) => {
|
|
20
|
+
if (a instanceof Error) return a.stack || `${a.name}: ${a.message}`;
|
|
21
|
+
if (typeof a === "object" && a !== null) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.stringify(a).slice(0, 500);
|
|
24
|
+
} catch {
|
|
25
|
+
return String(a);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return String(a);
|
|
29
|
+
};
|
|
30
|
+
const report = (level, args) => post({ type: "console", level, text: args.map(show).join(" ").slice(0, 2000), at: Date.now() });
|
|
31
|
+
for (const level of ["error", "warn"]) {
|
|
32
|
+
const original = console[level];
|
|
33
|
+
console[level] = (...args) => {
|
|
34
|
+
report(level, args);
|
|
35
|
+
return original.apply(console, args);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
addEventListener(
|
|
39
|
+
"error",
|
|
40
|
+
(e) => {
|
|
41
|
+
// An image, script or stylesheet that fails to load fires a plain error event on its element.
|
|
42
|
+
const el = e.target;
|
|
43
|
+
if (el instanceof Element) {
|
|
44
|
+
const url = [el.currentSrc, el.src, el.href].find((u) => typeof u === "string" && u) || el.getAttribute("href") || "";
|
|
45
|
+
return report("network", [`failed to load ${el.tagName.toLowerCase()} ${url}`.trim()]);
|
|
46
|
+
}
|
|
47
|
+
report("error", [e.error || e.message || "Script error"]);
|
|
48
|
+
},
|
|
49
|
+
true,
|
|
50
|
+
);
|
|
51
|
+
addEventListener("unhandledrejection", (e) => report("error", ["Unhandled promise rejection:", e.reason]));
|
|
52
|
+
const originalFetch = window.fetch;
|
|
53
|
+
if (originalFetch) {
|
|
54
|
+
window.fetch = async (input, init) => {
|
|
55
|
+
const url = typeof input === "string" ? input : input instanceof Request ? input.url : String(input);
|
|
56
|
+
const method = (init && init.method) || (input instanceof Request ? input.method : "GET");
|
|
57
|
+
try {
|
|
58
|
+
const res = await originalFetch(input, init);
|
|
59
|
+
if (res.status >= 400) report("network", [`${res.status} ${method} ${url}`]);
|
|
60
|
+
return res;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
report("network", [`${method} ${url} failed: ${err && err.message}`]);
|
|
63
|
+
throw err;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// One listener per request object, which a page may open again and again: it reports the
|
|
68
|
+
// latest open. It listens for the request being done, which comes before load and loadend,
|
|
69
|
+
// so a page that opens the next request from those still has this one reported.
|
|
70
|
+
const xhrOpen = XMLHttpRequest.prototype.open;
|
|
71
|
+
const opened = new WeakMap();
|
|
72
|
+
XMLHttpRequest.prototype.open = function (method, url, ...rest) {
|
|
73
|
+
if (!opened.has(this)) {
|
|
74
|
+
this.addEventListener("readystatechange", () => {
|
|
75
|
+
if (this.readyState !== XMLHttpRequest.DONE) return;
|
|
76
|
+
const { method, url } = opened.get(this);
|
|
77
|
+
if (this.status === 0) report("network", [`${method} ${url} failed`]);
|
|
78
|
+
else if (this.status >= 400) report("network", [`${this.status} ${method} ${url}`]);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
opened.set(this, { method, url });
|
|
82
|
+
return xhrOpen.call(this, method, url, ...rest);
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ------------------------------------------------------------ where the page is
|
|
86
|
+
const where = () => post({ type: "location", url: location.href, title: document.title });
|
|
87
|
+
for (const name of ["pushState", "replaceState"]) {
|
|
88
|
+
const original = history[name];
|
|
89
|
+
history[name] = function (...args) {
|
|
90
|
+
const result = original.apply(this, args);
|
|
91
|
+
setTimeout(where);
|
|
92
|
+
return result;
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
addEventListener("popstate", where);
|
|
96
|
+
addEventListener("hashchange", where);
|
|
97
|
+
|
|
98
|
+
// ------------------------------------------------------------ the layer: highlight, box, pins
|
|
99
|
+
// code-flow's paper palette, as literals (this runs inside other apps, with none of its CSS): mint
|
|
100
|
+
// outlines, ink labels and numbers with a white ring, so they read on light and dark pages alike.
|
|
101
|
+
// The panel's snapshot boxes and the PNG sent to agents use the same (ui/browser.tsx, ui/styles.css).
|
|
102
|
+
const MARK = "#2a9b72";
|
|
103
|
+
const MARK_FILL = "rgba(42,155,114,.1)";
|
|
104
|
+
const INK = "#343932";
|
|
105
|
+
const ON_INK = "#fbfaf6";
|
|
106
|
+
const SANS = 'Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",system-ui,sans-serif';
|
|
107
|
+
let layer;
|
|
108
|
+
const ensureLayer = () => {
|
|
109
|
+
if (layer && layer.isConnected) return layer;
|
|
110
|
+
layer = document.createElement("div");
|
|
111
|
+
layer.setAttribute("data-codeflow", "");
|
|
112
|
+
layer.style.cssText = "position:absolute;left:0;top:0;width:0;height:0;z-index:2147483647;pointer-events:none;";
|
|
113
|
+
(document.body || document.documentElement).appendChild(layer);
|
|
114
|
+
return layer;
|
|
115
|
+
};
|
|
116
|
+
const box = (cls) => {
|
|
117
|
+
const el = document.createElement("div");
|
|
118
|
+
el.className = cls;
|
|
119
|
+
el.style.cssText = `position:absolute;border:2px solid ${MARK};border-radius:6px;background:${MARK_FILL};box-sizing:border-box;pointer-events:none;`;
|
|
120
|
+
ensureLayer().appendChild(el);
|
|
121
|
+
return el;
|
|
122
|
+
};
|
|
123
|
+
const label = document.createElement("div");
|
|
124
|
+
label.style.cssText = `position:absolute;transform:translateY(calc(-100% - 4px));background:${INK};color:${ON_INK};font:500 11px/1.4 ui-monospace,"SFMono-Regular",Menlo,monospace;padding:2px 7px;border-radius:6px;box-shadow:0 1px 2px rgba(34,35,24,.12),0 4px 12px rgba(34,35,24,.14);white-space:nowrap;pointer-events:none;`;
|
|
125
|
+
let highlight;
|
|
126
|
+
const docRect = (r) => ({ x: Math.round(r.left + scrollX), y: Math.round(r.top + scrollY), w: Math.round(r.width), h: Math.round(r.height) });
|
|
127
|
+
const place = (el, r) => Object.assign(el.style, { left: `${r.x}px`, top: `${r.y}px`, width: `${r.w}px`, height: `${r.h}px` });
|
|
128
|
+
|
|
129
|
+
const ours = (el) => !!(el && el.closest && el.closest("[data-codeflow]"));
|
|
130
|
+
const selectorOf = (el) => {
|
|
131
|
+
const parts = [];
|
|
132
|
+
for (let node = el; node && node.nodeType === 1 && node !== document.body && parts.length < 5; node = node.parentElement) {
|
|
133
|
+
if (node.id && /^[A-Za-z][\w-]*$/.test(node.id)) {
|
|
134
|
+
parts.unshift(`#${node.id}`);
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
let part = node.tagName.toLowerCase();
|
|
138
|
+
const classes = [...node.classList].filter((c) => /^[A-Za-z][\w-]*$/.test(c) && c.length < 32).slice(0, 2);
|
|
139
|
+
if (classes.length) part += `.${classes.join(".")}`;
|
|
140
|
+
const same = node.parentElement ? [...node.parentElement.children].filter((c) => c.tagName === node.tagName) : [];
|
|
141
|
+
if (same.length > 1) part += `:nth-of-type(${same.indexOf(node) + 1})`;
|
|
142
|
+
parts.unshift(part);
|
|
143
|
+
}
|
|
144
|
+
return parts.join(" > ") || el.tagName.toLowerCase();
|
|
145
|
+
};
|
|
146
|
+
let recording = false;
|
|
147
|
+
const sensitive = el => /password|hidden|file/.test(el.type || "") || /password|secret|token|credential|cc-|one-time-code/i.test(`${el.id} ${el.name} ${el.autocomplete}`);
|
|
148
|
+
const record = (kind, el, value) => {
|
|
149
|
+
if (!recording || mode !== "off" || !(el instanceof Element) || ours(el)) return;
|
|
150
|
+
if (sensitive(el)) { post({ type: "recording-warning", text: "A sensitive field was excluded. Authenticated flows may require a test fixture." }); return; }
|
|
151
|
+
post({ type: "record-action", action: { kind, selector: selectorOf(el), ...(value !== undefined ? { value: String(value).slice(0, 2000) } : {}) } });
|
|
152
|
+
};
|
|
153
|
+
addEventListener("click", event => record("click", event.target.closest?.("button,a,input,select,[role=button]") || event.target), true);
|
|
154
|
+
addEventListener("input", event => { if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) { if (!["checkbox", "radio", "range", "color"].includes(event.target.type)) record("fill", event.target, event.target.value); } }, true);
|
|
155
|
+
addEventListener("change", event => { if (event.target instanceof HTMLSelectElement) record("select", event.target, event.target.value); }, true);
|
|
156
|
+
addEventListener("keydown", event => { if (["Enter", "Tab", "Escape"].includes(event.key)) record("press", event.target, event.key); }, true);
|
|
157
|
+
|
|
158
|
+
const STYLES = ["color", "background-color", "font-family", "font-size", "font-weight", "line-height", "padding", "margin", "border", "border-radius", "display", "gap", "width", "height", "opacity"];
|
|
159
|
+
const stylesOf = (el) => {
|
|
160
|
+
const cs = getComputedStyle(el);
|
|
161
|
+
return Object.fromEntries(STYLES.map((p) => [p, cs.getPropertyValue(p)]).filter(([, v]) => v && v !== "normal" && v !== "none" && v !== "auto"));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// ------------------------------------------------------------ snapshot: the DOM drawn into a PNG
|
|
165
|
+
// An SVG image of a styled copy of the page; outside images and fonts don't load in it, and a
|
|
166
|
+
// page that taints the canvas gets no snapshot. Best effort, never in the way.
|
|
167
|
+
const SNAP = ["display", "position", "box-sizing", "width", "height", "min-height", "margin", "padding", "border", "border-radius", "background", "background-color", "color", "font", "font-family", "font-size", "font-weight", "line-height", "letter-spacing", "text-align", "text-decoration", "text-transform", "white-space", "flex", "flex-direction", "flex-wrap", "align-items", "justify-content", "gap", "grid-template-columns", "grid-template-rows", "opacity", "box-shadow", "overflow", "vertical-align", "list-style", "top", "left", "right", "bottom", "transform", "z-index", "visibility"];
|
|
168
|
+
const MAX_NODES = 1500;
|
|
169
|
+
const styledCopy = (node) => {
|
|
170
|
+
let count = 0;
|
|
171
|
+
const copy = (src) => {
|
|
172
|
+
if (count++ > MAX_NODES) return document.createTextNode("");
|
|
173
|
+
if (src.nodeType === 3) return document.createTextNode(src.nodeValue);
|
|
174
|
+
if (src.nodeType !== 1 || ours(src) || src.tagName === "SCRIPT" || src.tagName === "STYLE" || src.tagName === "IFRAME") return document.createTextNode("");
|
|
175
|
+
const el = document.createElement(src.tagName === "BODY" || src.tagName === "HTML" ? "div" : src.tagName);
|
|
176
|
+
const cs = getComputedStyle(src);
|
|
177
|
+
el.setAttribute("style", SNAP.map((p) => `${p}:${cs.getPropertyValue(p)}`).join(";"));
|
|
178
|
+
if (src.tagName === "INPUT") {
|
|
179
|
+
el.setAttribute("type", src.type);
|
|
180
|
+
if (src.placeholder) el.setAttribute("placeholder", src.placeholder);
|
|
181
|
+
if (src.type === "checkbox" || src.type === "radio") {
|
|
182
|
+
if (src.checked) el.setAttribute("checked", "");
|
|
183
|
+
} else {
|
|
184
|
+
// A password never goes into a snapshot: dots stand in for it.
|
|
185
|
+
el.setAttribute("value", src.type === "password" ? "\u2022".repeat(src.value.length) : src.value);
|
|
186
|
+
}
|
|
187
|
+
return el;
|
|
188
|
+
}
|
|
189
|
+
if (src.tagName === "TEXTAREA") {
|
|
190
|
+
el.textContent = src.value;
|
|
191
|
+
return el;
|
|
192
|
+
}
|
|
193
|
+
for (const child of src.childNodes) el.appendChild(copy(child));
|
|
194
|
+
return el;
|
|
195
|
+
};
|
|
196
|
+
return copy(node);
|
|
197
|
+
};
|
|
198
|
+
const snapshot = async (node, rect) => {
|
|
199
|
+
try {
|
|
200
|
+
const scale = Math.min(1, 1600 / Math.max(rect.w, 1), 1200 / Math.max(rect.h, 1));
|
|
201
|
+
const w = Math.max(1, Math.round(rect.w * scale));
|
|
202
|
+
const h = Math.max(1, Math.round(rect.h * scale));
|
|
203
|
+
const frame = document.createElement("div");
|
|
204
|
+
frame.setAttribute("xmlns", "http://www.w3.org/1999/xhtml");
|
|
205
|
+
if (node === document.body) {
|
|
206
|
+
// A box on the page: the whole body, moved so the box is at the top left.
|
|
207
|
+
frame.style.cssText = `width:${document.documentElement.scrollWidth}px;transform:translate(${-rect.x}px,${-rect.y}px);background:${getComputedStyle(document.body).backgroundColor || "#fff"}`;
|
|
208
|
+
} else {
|
|
209
|
+
frame.style.cssText = `width:${rect.w}px;height:${rect.h}px;overflow:hidden`;
|
|
210
|
+
}
|
|
211
|
+
frame.appendChild(styledCopy(node));
|
|
212
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${rect.w} ${rect.h}"><foreignObject width="${node === document.body ? document.documentElement.scrollWidth : rect.w}" height="${node === document.body ? document.documentElement.scrollHeight : rect.h}">${new XMLSerializer().serializeToString(frame)}</foreignObject></svg>`;
|
|
213
|
+
const img = new Image();
|
|
214
|
+
img.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
215
|
+
await new Promise((resolve, reject) => { img.onload = resolve; img.onerror = reject; if (img.complete && img.naturalWidth) resolve(); });
|
|
216
|
+
const canvas = document.createElement("canvas");
|
|
217
|
+
canvas.width = w;
|
|
218
|
+
canvas.height = h;
|
|
219
|
+
const ctx = canvas.getContext("2d");
|
|
220
|
+
ctx.fillStyle = getComputedStyle(document.body).backgroundColor || "#fff";
|
|
221
|
+
ctx.fillRect(0, 0, w, h);
|
|
222
|
+
ctx.drawImage(img, 0, 0, w, h);
|
|
223
|
+
return canvas.toDataURL("image/png");
|
|
224
|
+
} catch {
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/** An element's HTML, without what's typed in its password fields (frameworks copy it into the value attribute). */
|
|
230
|
+
const htmlOf = (el) => {
|
|
231
|
+
const passwords = (node) => [...(node.matches("input[type=password]") ? [node] : []), ...node.querySelectorAll("input[type=password]")];
|
|
232
|
+
if (!passwords(el).length) return el.outerHTML;
|
|
233
|
+
const clone = el.cloneNode(true);
|
|
234
|
+
for (const input of passwords(clone)) input.removeAttribute("value");
|
|
235
|
+
return clone.outerHTML;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
// ------------------------------------------------------------ pick and box
|
|
239
|
+
let mode = "off";
|
|
240
|
+
const picked = new Map();
|
|
241
|
+
// Pin ids are new in every page load, so a pin the panel kept from an earlier page never names an element on this one.
|
|
242
|
+
const load = Math.random().toString(36).slice(2, 10);
|
|
243
|
+
let nextId = 1;
|
|
244
|
+
const viewport = () => ({ w: innerWidth, h: innerHeight });
|
|
245
|
+
const pin = async (kind, el, rect) => {
|
|
246
|
+
const id = `${load}-${nextId++}`;
|
|
247
|
+
if (kind === "element") picked.set(id, { el, original: el.style.cssText });
|
|
248
|
+
post({ type: "pin-start", id });
|
|
249
|
+
const image = await snapshot(kind === "element" ? el : document.body, rect);
|
|
250
|
+
post({
|
|
251
|
+
type: "pin",
|
|
252
|
+
pin: {
|
|
253
|
+
id,
|
|
254
|
+
kind,
|
|
255
|
+
selector: selectorOf(el),
|
|
256
|
+
text: (el.innerText || el.textContent || "").trim().replace(/\s+/g, " ").slice(0, 200),
|
|
257
|
+
html: kind === "element" ? htmlOf(el).slice(0, 600) : undefined,
|
|
258
|
+
styles: kind === "element" ? stylesOf(el) : undefined,
|
|
259
|
+
rect,
|
|
260
|
+
url: location.href,
|
|
261
|
+
viewport: viewport(),
|
|
262
|
+
image,
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
const onMove = (e) => {
|
|
267
|
+
if (mode !== "pick") return;
|
|
268
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
269
|
+
if (!el || ours(el)) return;
|
|
270
|
+
highlight = highlight && highlight.isConnected ? highlight : box("cf-highlight");
|
|
271
|
+
const r = docRect(el.getBoundingClientRect());
|
|
272
|
+
place(highlight, r);
|
|
273
|
+
label.textContent = selectorOf(el);
|
|
274
|
+
Object.assign(label.style, { left: `${r.x}px`, top: `${r.y}px` });
|
|
275
|
+
ensureLayer().appendChild(label);
|
|
276
|
+
};
|
|
277
|
+
const swallow = (e) => {
|
|
278
|
+
if (mode === "off" || ours(e.target)) return;
|
|
279
|
+
e.preventDefault();
|
|
280
|
+
e.stopPropagation();
|
|
281
|
+
};
|
|
282
|
+
const onClick = (e) => {
|
|
283
|
+
if (mode !== "pick" || ours(e.target)) return;
|
|
284
|
+
swallow(e);
|
|
285
|
+
const el = document.elementFromPoint(e.clientX, e.clientY);
|
|
286
|
+
if (el && !ours(el)) void pin("element", el, docRect(el.getBoundingClientRect()));
|
|
287
|
+
};
|
|
288
|
+
let drag;
|
|
289
|
+
const onDown = (e) => {
|
|
290
|
+
if (mode !== "box" || e.button !== 0) return;
|
|
291
|
+
swallow(e);
|
|
292
|
+
drag = { x: e.clientX + scrollX, y: e.clientY + scrollY, el: box("cf-drag") };
|
|
293
|
+
drag.el.style.borderStyle = "dashed";
|
|
294
|
+
};
|
|
295
|
+
const onDrag = (e) => {
|
|
296
|
+
if (!drag) return;
|
|
297
|
+
const x = e.clientX + scrollX;
|
|
298
|
+
const y = e.clientY + scrollY;
|
|
299
|
+
place(drag.el, { x: Math.min(x, drag.x), y: Math.min(y, drag.y), w: Math.abs(x - drag.x), h: Math.abs(y - drag.y) });
|
|
300
|
+
};
|
|
301
|
+
const onUp = (e) => {
|
|
302
|
+
if (!drag) return;
|
|
303
|
+
swallow(e);
|
|
304
|
+
const x = e.clientX + scrollX;
|
|
305
|
+
const y = e.clientY + scrollY;
|
|
306
|
+
const rect = { x: Math.min(x, drag.x), y: Math.min(y, drag.y), w: Math.abs(x - drag.x), h: Math.abs(y - drag.y) };
|
|
307
|
+
drag.el.remove();
|
|
308
|
+
drag = undefined;
|
|
309
|
+
if (rect.w < 8 || rect.h < 8) return;
|
|
310
|
+
const center = document.elementFromPoint(rect.x + rect.w / 2 - scrollX, rect.y + rect.h / 2 - scrollY) || document.body;
|
|
311
|
+
void pin("box", ours(center) ? document.body : center, rect);
|
|
312
|
+
};
|
|
313
|
+
const setMode = (next) => {
|
|
314
|
+
mode = next;
|
|
315
|
+
document.documentElement.style.cursor = mode === "off" ? "" : "crosshair";
|
|
316
|
+
if (mode !== "pick") {
|
|
317
|
+
if (highlight) highlight.remove();
|
|
318
|
+
label.remove();
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
// Pointer events for the box. Cancelling a press stops the mouse events that would follow it,
|
|
322
|
+
// but not the click (or a right or middle click), so those are swallowed too.
|
|
323
|
+
addEventListener("mousemove", onMove, true);
|
|
324
|
+
addEventListener("pointermove", onDrag, true);
|
|
325
|
+
addEventListener("click", onClick, true);
|
|
326
|
+
addEventListener("pointerdown", onDown, true);
|
|
327
|
+
addEventListener("pointerup", onUp, true);
|
|
328
|
+
for (const type of ["pointerdown", "pointerup", "mousedown", "mouseup", "click", "auxclick", "contextmenu", "dblclick", "submit"]) addEventListener(type, swallow, true);
|
|
329
|
+
addEventListener("keydown", (e) => {
|
|
330
|
+
if (e.key === "Escape" && mode !== "off") {
|
|
331
|
+
setMode("off");
|
|
332
|
+
post({ type: "mode", mode: "off" });
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
/** The numbered markers, where the panel says its pins are. */
|
|
337
|
+
const drawPins = (pins) => {
|
|
338
|
+
ensureLayer();
|
|
339
|
+
for (const old of layer.querySelectorAll(".cf-pin, .cf-pinbox")) old.remove();
|
|
340
|
+
for (const p of pins) {
|
|
341
|
+
const outline = box("cf-pinbox");
|
|
342
|
+
place(outline, p.rect);
|
|
343
|
+
outline.style.borderStyle = p.kind === "box" ? "dashed" : "solid";
|
|
344
|
+
outline.style.background = "transparent";
|
|
345
|
+
const dot = document.createElement("div");
|
|
346
|
+
dot.className = "cf-pin";
|
|
347
|
+
dot.textContent = String(p.n);
|
|
348
|
+
dot.style.cssText = `position:absolute;left:${p.rect.x + p.rect.w - 9}px;top:${p.rect.y - 9}px;width:18px;height:18px;border-radius:50%;background:${INK};color:${ON_INK};font:600 11px ${SANS};display:grid;place-items:center;box-shadow:0 0 0 2px #fff;pointer-events:none;`;
|
|
349
|
+
layer.appendChild(dot);
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
/** Adjust: try style changes on a picked element; an empty value puts the page's own back. */
|
|
354
|
+
const adjust = (id, styles) => {
|
|
355
|
+
const entry = picked.get(id);
|
|
356
|
+
if (!entry) return post({ type: "adjust-gone", id });
|
|
357
|
+
for (const [prop, value] of Object.entries(styles)) {
|
|
358
|
+
if (value) entry.el.style.setProperty(prop, value);
|
|
359
|
+
else entry.el.style.removeProperty(prop);
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
const reset = (id) => {
|
|
363
|
+
const entry = picked.get(id);
|
|
364
|
+
if (!entry) return;
|
|
365
|
+
// Through the style object, not the attribute: a page whose policy forbids inline styles still allows this.
|
|
366
|
+
entry.el.style.cssText = entry.original;
|
|
367
|
+
if (!entry.original) entry.el.removeAttribute("style");
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
addEventListener("message", (e) => {
|
|
371
|
+
const m = e.data;
|
|
372
|
+
if (e.source !== window.parent || !PARENTS.includes(e.origin) || !m || m.codeflow !== 1) return;
|
|
373
|
+
if (m.type === "hello") {
|
|
374
|
+
parent = e.origin;
|
|
375
|
+
post({ type: "ready", url: location.href, title: document.title });
|
|
376
|
+
for (const msg of early.splice(0)) post(msg);
|
|
377
|
+
} else if (m.type === "recording") recording = m.active === true;
|
|
378
|
+
else if (m.type === "mode") setMode(m.mode);
|
|
379
|
+
else if (m.type === "pins") drawPins(m.pins || []);
|
|
380
|
+
else if (m.type === "adjust") adjust(m.id, m.styles || {});
|
|
381
|
+
else if (m.type === "reset") reset(m.id);
|
|
382
|
+
});
|
|
383
|
+
})();
|