@codeyam-editor/codeyam-editor 0.1.12-staging.g83e4a0c → 0.1.12-staging.gf52ff7a
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/npm/fleet-build-data.js +8 -1
- package/npm/fleet-job-badge.js +32 -1
- package/npm/fleet-live-status.js +13 -0
- package/npm/fleet-notify.js +130 -0
- package/npm/fleet-queue.js +57 -0
- package/npm/fleet-reset.js +13 -1
- package/npm/fleet-stuck-status.js +8 -0
- package/npm/launch-config.js +77 -12
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-DSrne6Fx.js → ContainerInspector-B86bl6-K.js} +1 -1
- package/ui/dist/assets/EditorShell-BLCavSsd.js +284 -0
- package/ui/dist/assets/ScenarioDataPanel-BOz-4rZe.js +21 -0
- package/ui/dist/assets/{ScenarioDataPanelFullPage-Dg0rrTMh.js → ScenarioDataPanelFullPage-Dy8mPj3e.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-CmcR8ciQ.js +26 -0
- package/ui/dist/assets/index-D2tD0_8V.css +1 -0
- package/ui/dist/assets/{index-ngNVAM6P.js → index-maa9OUlU.js} +2 -2
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/EditorShell-DknIcmDU.js +0 -332
- package/ui/dist/assets/ScenarioDataPanel-DCBa1wAP.js +0 -21
- package/ui/dist/assets/ScenarioRenderer-QRzqzyUm.js +0 -26
- package/ui/dist/assets/index-DvkmhQJ8.css +0 -1
package/npm/fleet-build-data.js
CHANGED
|
@@ -84,6 +84,13 @@ function buildVmEnvelope(v, ctx) {
|
|
|
84
84
|
step: v.step != null ? v.step : null,
|
|
85
85
|
mode: v.mode || null,
|
|
86
86
|
label: v.label || null,
|
|
87
|
+
// What the last closed cycle was, { feature, closedAt }, from the editor's
|
|
88
|
+
// `resumeMode: "completed"` shape. Null when the cycle is open, when it
|
|
89
|
+
// closed through `assist-complete` (which writes no marker), or on an editor
|
|
90
|
+
// that predates the field. The card names the finished feature from it.
|
|
91
|
+
cycleClosed: v.cycleClosed
|
|
92
|
+
? { feature: v.cycleClosed.feature || null, closedAt: v.cycleClosed.closedAt || null }
|
|
93
|
+
: null,
|
|
87
94
|
status: ctx.status,
|
|
88
95
|
orphan: ctx.orphan || null,
|
|
89
96
|
degradedReasons: ctx.degradedReasons || [],
|
|
@@ -184,7 +191,7 @@ function buildVmEnvelope(v, ctx) {
|
|
|
184
191
|
// if you do one without the other.
|
|
185
192
|
const REQUIRED_CARD_FIELDS = [
|
|
186
193
|
'n', 'url', 'previewUrl', 'appPort', 'previewServing', 'previewIngress', 'reachable', 'reachableSource', 'hasSession', 'brokerLive', 'brokerStatus',
|
|
187
|
-
'awaitingInput', 'needsCleanup', 'feature', 'selectedPlan', 'step', 'mode', 'label', 'status', 'orphan',
|
|
194
|
+
'awaitingInput', 'needsCleanup', 'feature', 'selectedPlan', 'step', 'mode', 'label', 'cycleClosed', 'status', 'orphan',
|
|
188
195
|
'degradedReasons', 'setupSteps', 'attentionReasons',
|
|
189
196
|
'isHead', 'headHolderUnreachable', 'queuePos', 'queueLen',
|
|
190
197
|
'job', 'config', 'project', 'variant', 'cardIdentity', 'brokerHistory',
|
package/npm/fleet-job-badge.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const faf = require('./fleet-add-failure');
|
|
4
|
+
|
|
3
5
|
// npm/fleet-job-badge.js — pure verdict for the fleet dashboard's failed-job
|
|
4
6
|
// badge reconciliation (improve39). A VM's last terminal-error heavy job
|
|
5
7
|
// (add/error, reset-all error, reset-client error, …) used to stick on the
|
|
@@ -96,4 +98,33 @@ function canClearBadge({ rostered, job } = {}) {
|
|
|
96
98
|
return { ok: true, reason: null, cleanup: 'badge-only' };
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
|
|
101
|
+
// The short label the card prints ahead of a terminal job's headline — the
|
|
102
|
+
// "<action> <state>" prefix, derived from the classifier's verdict rather than
|
|
103
|
+
// from the coarse `job.state` alone.
|
|
104
|
+
//
|
|
105
|
+
// job the carried job record ({ action, state, category, ... })
|
|
106
|
+
// wiringDeadlineBlown the add's wiring tail gave up (the calm-hold release)
|
|
107
|
+
//
|
|
108
|
+
// A terminal add error is NOT one fact. The classifier (npm/fleet-add-failure.js)
|
|
109
|
+
// splits it into outcomes PROVEN bad — which keep "add failed" — and the calm
|
|
110
|
+
// CALM_ADD_CATEGORIES side, where nothing proves the VM dead. Printing "add error"
|
|
111
|
+
// on the calm side produced a card contradicting itself: "add error · Add outcome
|
|
112
|
+
// unconfirmed", on a branch that is deliberately not retryable, so it named a
|
|
113
|
+
// fault and offered no fix (three healthy VMs, 2026-09-28). The calm label is
|
|
114
|
+
// gated on holdConvergingBadge — the SAME predicate deriveStatus uses to pick
|
|
115
|
+
// add-verifying/add-converging over add-failed — so the label and the status dot
|
|
116
|
+
// can never disagree about whether this is a failure.
|
|
117
|
+
//
|
|
118
|
+
// Only the LABEL changes. `job.state` stays 'error', so canRetryAdd,
|
|
119
|
+
// canClearBadge and shouldReconcileFailedBadge keep gating on it unchanged.
|
|
120
|
+
function jobBadgeLabel({ job, wiringDeadlineBlown } = {}) {
|
|
121
|
+
if (!job) return '';
|
|
122
|
+
if (job.action === 'add' && job.state === 'error') {
|
|
123
|
+
return faf.holdConvergingBadge({ category: job.category, wiringDeadlineBlown })
|
|
124
|
+
? 'add unconfirmed'
|
|
125
|
+
: 'add failed';
|
|
126
|
+
}
|
|
127
|
+
return `${job.action} ${job.state}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { shouldReconcileFailedBadge, shouldClearReconciledBadge, canClearBadge, jobBadgeLabel };
|
package/npm/fleet-live-status.js
CHANGED
|
@@ -31,6 +31,17 @@
|
|
|
31
31
|
// mid-feature post-restart agent is not terminal (`needsCleanup` falsy) and stays
|
|
32
32
|
// 'building', preserving the 2026-06-20 decoupled-agent fix above.
|
|
33
33
|
//
|
|
34
|
+
// The closed-cycle refinement (fleet--fix-a-finished-vm-is-never-flagged): both
|
|
35
|
+
// `feature-complete` and `assist-complete` delete `editor-step.json` but keep the
|
|
36
|
+
// saved build-agent session, so /api/session-info answers with the
|
|
37
|
+
// `resumeMode: "completed"` shape — `hasSession: true`, no awaiting flag, no
|
|
38
|
+
// `needsCleanup`. That shape used to fall straight into the `hasSession ⇒
|
|
39
|
+
// 'building'` line, so a finished VM read "building" forever and was never
|
|
40
|
+
// flagged. We key on `resumeMode`, not on the `cycleClosed` marker, because
|
|
41
|
+
// only `feature-complete` writes the marker. No broker check is needed to tell
|
|
42
|
+
// a parked agent from a working one: the moment the agent takes a new request
|
|
43
|
+
// it enters triage, which writes step state and retires the completed shape.
|
|
44
|
+
//
|
|
34
45
|
// Returns 'waiting' | 'building' | 'session-done' | 'idle' for a reachable VM, or
|
|
35
46
|
// `null` when the VM is unreachable — the caller maps null to its own
|
|
36
47
|
// asleep/unreachable verdict (it owns the running-fleet snapshot this module
|
|
@@ -43,6 +54,8 @@ function liveSessionStatus(v, brokerStatus) {
|
|
|
43
54
|
// asleep/unreachable mapping — a stale broker signal must not mask it.
|
|
44
55
|
if (!v.reachable) return null;
|
|
45
56
|
if (v.hasSession && v.awaitingUserInput) return awaitingVerdict(v);
|
|
57
|
+
// The cycle closed and nothing has started since — finished, review it.
|
|
58
|
+
if (v.resumeMode === 'completed') return 'session-done';
|
|
46
59
|
if (v.hasSession) return 'building';
|
|
47
60
|
// No editor-owned session, but a live agent may still be working in the PTY
|
|
48
61
|
// broker (decoupled lifetime — see docs/process-model.md). The broker
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Push alerts for the Fleet Dashboard: one phone notification each time a VM
|
|
3
|
+
// becomes "needs you" — its cycle finished, or it has sat on a gate past the
|
|
4
|
+
// needs-you threshold (npm/fleet-stuck-status.js decides both). server.js does
|
|
5
|
+
// the I/O; the decision here is pure so the dedupe rules are unit-testable.
|
|
6
|
+
//
|
|
7
|
+
// Why the dashboard and not each VM: the operator host already sees the whole
|
|
8
|
+
// fleet, so one topic in the operator's own `$HOME/.codeyam/notify.env` covers
|
|
9
|
+
// every VM. Provisioning the topic onto each VM was rejected — a client VM's
|
|
10
|
+
// container must never hold the operator's topic.
|
|
11
|
+
//
|
|
12
|
+
// Dedupe: each VM's last alert is remembered as a KEY naming the episode — the
|
|
13
|
+
// cycle's close time for a finished VM, the step's start time for a waiting
|
|
14
|
+
// one. A 3s poll, a dashboard restart (the sent-set is persisted by the
|
|
15
|
+
// caller), or a status flap back to the same episode never re-sends; a new
|
|
16
|
+
// cycle or a new gate carries a new key and does.
|
|
17
|
+
|
|
18
|
+
// The episode a needs-you card belongs to. An assist cycle writes no close
|
|
19
|
+
// marker, so its key is just 'done:' — see the building rule in
|
|
20
|
+
// decideNotifications for how that episode ends.
|
|
21
|
+
function episodeKey(env) {
|
|
22
|
+
if (env.status === 'session-done') {
|
|
23
|
+
return 'done:' + ((env.cycleClosed && env.cycleClosed.closedAt) || '');
|
|
24
|
+
}
|
|
25
|
+
return 'waiting:' + (env.startedAt || '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function alertFor(env, key) {
|
|
29
|
+
const vm = 'VM-' + env.n;
|
|
30
|
+
if (env.status === 'session-done') {
|
|
31
|
+
const feature = (env.cycleClosed && env.cycleClosed.feature) || env.feature;
|
|
32
|
+
return {
|
|
33
|
+
n: env.n,
|
|
34
|
+
key,
|
|
35
|
+
title: vm + ' finished',
|
|
36
|
+
message: feature ? vm + ' finished: ' + feature : vm + ' finished its work and is ready for review',
|
|
37
|
+
clickUrl: env.url || null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const what = env.label || env.feature;
|
|
41
|
+
return {
|
|
42
|
+
n: env.n,
|
|
43
|
+
key,
|
|
44
|
+
title: vm + ' is waiting on you',
|
|
45
|
+
message: what ? vm + ' is waiting on you: ' + what : vm + ' is waiting on you',
|
|
46
|
+
clickUrl: env.url || null,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// decideNotifications(prevSent, envelopes) -> { toSend, nextSent, changed }
|
|
51
|
+
//
|
|
52
|
+
// prevSent — { [n]: { key } } persisted from the previous call.
|
|
53
|
+
// envelopes — the per-VM card envelopes buildData produced this poll.
|
|
54
|
+
//
|
|
55
|
+
// Rules:
|
|
56
|
+
// - A needs-you card whose episode key differs from the remembered one is
|
|
57
|
+
// sent, and its key remembered.
|
|
58
|
+
// - A needs-you card whose key matches is skipped.
|
|
59
|
+
// - A card that is NOT needs-you keeps its remembered key, so a flap through
|
|
60
|
+
// unreachable/reconnecting back into the same episode stays quiet.
|
|
61
|
+
// - Except: a card back to 'building' after an assist cycle (key 'done:')
|
|
62
|
+
// forgets it. That key cannot tell one assist cycle from the next, so the
|
|
63
|
+
// return to work is the only thing that ends the episode.
|
|
64
|
+
// - A VM absent from `envelopes` (destroyed) is forgotten.
|
|
65
|
+
// - `changed` says whether nextSent differs from prevSent, so the caller
|
|
66
|
+
// writes the persisted copy only when there is something new to write.
|
|
67
|
+
function decideNotifications(prevSent, envelopes) {
|
|
68
|
+
const prev = prevSent || {};
|
|
69
|
+
const toSend = [];
|
|
70
|
+
const nextSent = {};
|
|
71
|
+
for (const env of envelopes || []) {
|
|
72
|
+
if (!env || env.n == null) continue;
|
|
73
|
+
const remembered = prev[env.n] || null;
|
|
74
|
+
const needsYou = !!(env.attention && env.attention.needsYou);
|
|
75
|
+
if (needsYou) {
|
|
76
|
+
const key = episodeKey(env);
|
|
77
|
+
if (!remembered || remembered.key !== key) toSend.push(alertFor(env, key));
|
|
78
|
+
nextSent[env.n] = { key };
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (remembered && !(env.status === 'building' && remembered.key === 'done:')) {
|
|
82
|
+
nextSent[env.n] = remembered;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const changed = JSON.stringify(nextSent) !== JSON.stringify(prev);
|
|
86
|
+
return { toSend, nextSent, changed };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The dashboard log line for one send attempt, so the operator can see in the
|
|
90
|
+
// dashboard log which alerts went out and why one did not.
|
|
91
|
+
function formatSendResult(item, result) {
|
|
92
|
+
if (result.ok) return `[notify] VM-${item.n}: ${item.title}`;
|
|
93
|
+
return `[notify] VM-${item.n} push failed: HTTP ${result.status}${result.error ? ' ' + result.error : ''}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Parse the operator's notify config the same way .claude/hooks/claude-notify.sh
|
|
97
|
+
// resolves it: an exported NTFY_TOPIC wins, else `$HOME/.codeyam/notify.env`
|
|
98
|
+
// (KEY=value lines, as `editor notify-setup` writes them). Returns null when no
|
|
99
|
+
// topic is configured — the caller then sends nothing, silently, like the hook.
|
|
100
|
+
function resolveNotifyConfig(env, notifyEnvText) {
|
|
101
|
+
const e = env || {};
|
|
102
|
+
const fileVals = {};
|
|
103
|
+
for (const raw of String(notifyEnvText || '').split('\n')) {
|
|
104
|
+
const line = raw.trim().replace(/^export\s+/, '');
|
|
105
|
+
const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line);
|
|
106
|
+
if (!m) continue;
|
|
107
|
+
fileVals[m[1]] = m[2].trim().replace(/^(['"])(.*)\1$/, '$2');
|
|
108
|
+
}
|
|
109
|
+
const topic = e.NTFY_TOPIC || fileVals.NTFY_TOPIC || '';
|
|
110
|
+
if (!topic) return null;
|
|
111
|
+
const server = e.NTFY_SERVER || fileVals.NTFY_SERVER || 'https://ntfy.sh';
|
|
112
|
+
return { topic, server: server.replace(/\/+$/, '') };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Send one alert to ntfy with the same headers claude-notify.sh uses. Resolves
|
|
116
|
+
// to { ok, status } and never rejects: a failed push is logged by the caller,
|
|
117
|
+
// never allowed to break a poll.
|
|
118
|
+
async function sendNtfy(cfg, item, fetchImpl) {
|
|
119
|
+
const doFetch = fetchImpl || fetch;
|
|
120
|
+
const headers = { Title: item.title };
|
|
121
|
+
if (item.clickUrl) headers.Click = item.clickUrl;
|
|
122
|
+
try {
|
|
123
|
+
const res = await doFetch(cfg.server + '/' + cfg.topic, { method: 'POST', headers, body: item.message });
|
|
124
|
+
return { ok: res.status === 200, status: res.status };
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return { ok: false, status: null, error: e && e.message };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
module.exports = { decideNotifications, formatSendResult, resolveNotifyConfig, sendNtfy };
|
package/npm/fleet-queue.js
CHANGED
|
@@ -262,7 +262,64 @@ function queueWriteBlockReason(memo = {}) {
|
|
|
262
262
|
: `commit queue read is failing${why} — the displayed order cannot be trusted; fix the read before changing the queue`;
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
/** Should this scheduled queue tick do any work at all?
|
|
266
|
+
*
|
|
267
|
+
* The scheduled poll used to fetch and exec `branch-queue status` every
|
|
268
|
+
* QUEUE_POLL_MS whether or not anyone had the dashboard open. On 2026-09-27
|
|
269
|
+
* that was 13,120 binary starts (~4.7h of process time) across one 13h audit
|
|
270
|
+
* window, competing with the audit for CPU. When no
|
|
271
|
+
* browser has requested `/data` for `idleAfterMs`, the tick runs at most once
|
|
272
|
+
* per `idleIntervalMs` — slow enough to cost nothing, fast enough that the
|
|
273
|
+
* gone-head auto-release (which rides this poll) still fires within a minute.
|
|
274
|
+
*
|
|
275
|
+
* `lastViewerAt` / `lastTickAt` are epoch ms or null. A null `lastViewerAt`
|
|
276
|
+
* (nobody has looked since boot) counts as idle; a null `lastTickAt` always
|
|
277
|
+
* ticks, so the boot read is never skipped. Pure; no I/O. */
|
|
278
|
+
function shouldRunQueueTick({ now, lastViewerAt, lastTickAt, idleAfterMs, idleIntervalMs } = {}) {
|
|
279
|
+
const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
|
|
280
|
+
const t = num(now);
|
|
281
|
+
const tick = num(lastTickAt);
|
|
282
|
+
if (t === null || tick === null) return true;
|
|
283
|
+
const viewer = num(lastViewerAt);
|
|
284
|
+
const watched = viewer !== null && t - viewer < (num(idleAfterMs) || 0);
|
|
285
|
+
if (watched) return true;
|
|
286
|
+
return t - tick >= (num(idleIntervalMs) || 0);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Does a freshly fetched queue-ref snapshot need a `branch-queue status` exec?
|
|
290
|
+
*
|
|
291
|
+
* `branch-queue status` is a pure projection of `refs/codeyam/queue/*`, so
|
|
292
|
+
* when those refs are byte-identical to the ones behind the last SUCCESSFUL
|
|
293
|
+
* read, the exec can only return the answer already in the memo — and each
|
|
294
|
+
* exec is a full binary start plus a second network fetch of the same refs.
|
|
295
|
+
* Skipping it is what turns a per-tick process start into a per-change one.
|
|
296
|
+
*
|
|
297
|
+
* Always exec when:
|
|
298
|
+
* - the fingerprint could not be computed (`null` — never trust a failed
|
|
299
|
+
* local read as "unchanged");
|
|
300
|
+
* - no successful read has happened (`memo.readAt` null) or the last read
|
|
301
|
+
* FAILED (`memo.error`) — a cached answer must never paper over an error,
|
|
302
|
+
* the 2026-08-14 frozen-panel lesson mergeQueuePoll records;
|
|
303
|
+
* - the refs moved since the fingerprint the memo was read against;
|
|
304
|
+
* - the last exec is older than `maxAgeMs` — a safety net so a projection
|
|
305
|
+
* that ever grows a non-ref input cannot freeze the panel indefinitely.
|
|
306
|
+
*
|
|
307
|
+
* Returns { exec: boolean, reason: string }. Pure; no I/O. */
|
|
308
|
+
function decideQueueStatusExec({ fingerprint, memo = {}, lastExecAt, now, maxAgeMs } = {}) {
|
|
309
|
+
if (typeof fingerprint !== 'string') return { exec: true, reason: 'fingerprint-unavailable' };
|
|
310
|
+
if (!(typeof memo.readAt === 'string' && memo.readAt)) return { exec: true, reason: 'never-read' };
|
|
311
|
+
if (typeof memo.error === 'string' && memo.error) return { exec: true, reason: 'last-read-failed' };
|
|
312
|
+
if (memo.fingerprint !== fingerprint) return { exec: true, reason: 'refs-changed' };
|
|
313
|
+
const at = (typeof lastExecAt === 'number' && Number.isFinite(lastExecAt)) ? lastExecAt : null;
|
|
314
|
+
const clock = (typeof now === 'number' && Number.isFinite(now)) ? now : null;
|
|
315
|
+
if (at === null || clock === null) return { exec: true, reason: 'max-age' };
|
|
316
|
+
if (clock - at >= (typeof maxAgeMs === 'number' ? maxAgeMs : 0)) return { exec: true, reason: 'max-age' };
|
|
317
|
+
return { exec: false, reason: 'refs-unchanged' };
|
|
318
|
+
}
|
|
319
|
+
|
|
265
320
|
module.exports = {
|
|
321
|
+
shouldRunQueueTick,
|
|
322
|
+
decideQueueStatusExec,
|
|
266
323
|
decideAutoReleaseGoneHead,
|
|
267
324
|
computeReorderTargets,
|
|
268
325
|
nudgeQueueOrder,
|
package/npm/fleet-reset.js
CHANGED
|
@@ -50,6 +50,14 @@
|
|
|
50
50
|
// repopulates over whatever remains). Emptying contents (not removing the dir)
|
|
51
51
|
// also sidesteps the `rmdir` race entirely.
|
|
52
52
|
//
|
|
53
|
+
// The wipe NEVER descends into `.codeyam/agent-home`. Beside the two gitignored
|
|
54
|
+
// infra files preserved above, it is the third thing that must survive: when
|
|
55
|
+
// the editor relocates Claude's sign-in store into the project it lives at
|
|
56
|
+
// `.codeyam/agent-home/claude`, and it holds a live sign-in no restore step
|
|
57
|
+
// could recreate — wiping it strands the VM at `claude /login`. It is excluded
|
|
58
|
+
// rather than round-tripped through a world-readable `/tmp` like the two files;
|
|
59
|
+
// an untouched directory needs no restore, so the trap recovery is unchanged.
|
|
60
|
+
//
|
|
53
61
|
// The destructive region (park → broker-stop → wipe → restore) is made
|
|
54
62
|
// FAILURE-SAFE with an EXIT trap installed before the park and disarmed after
|
|
55
63
|
// a successful restore. If ANY step in that region aborts the chain, the trap
|
|
@@ -83,7 +91,11 @@ function codeyamDeepCleanSteps(E) {
|
|
|
83
91
|
`{ __cy_recover() { ${recover}; }; trap '__cy_recover; exit 1' EXIT; }`,
|
|
84
92
|
`(${E} editor stop-server --hold-for-restart --force-in-container || true)`,
|
|
85
93
|
`(${E} editor pty-broker stop || true)`,
|
|
86
|
-
|
|
94
|
+
// Spares `.codeyam/agent-home` (see the header): `! -path` rather than
|
|
95
|
+
// `-prune`, because `-delete` implies `-depth`, under which `-prune` is a
|
|
96
|
+
// no-op. The done-check ignores it too, or a relocated VM would always
|
|
97
|
+
// spin the full retry budget.
|
|
98
|
+
'( for i in 1 2 3 4 5; do find .codeyam -mindepth 1 ! -path .codeyam/agent-home ! -path ".codeyam/agent-home/*" -delete 2>/dev/null; [ -z "$(ls -A .codeyam 2>/dev/null | grep -vx agent-home)" ] && break; sleep 0.2; done; true )',
|
|
87
99
|
`( ${restoreCodeyam} )`,
|
|
88
100
|
`( ${restoreElj} && rm -f /tmp/cy-elj.bak; true )`,
|
|
89
101
|
`( ${restoreSs} && rm -f /tmp/cy-ss.bak; true )`,
|
|
@@ -78,6 +78,11 @@ function elapsedMs(iso, nowMs) {
|
|
|
78
78
|
// longer drives this: liveSessionStatus already collapses an idle awaiting
|
|
79
79
|
// agent to 'idle' and a blocked one to 'waiting', so the status IS the
|
|
80
80
|
// discriminator.
|
|
81
|
+
// - A 'session-done' VM (its cycle closed) is "needs you" IMMEDIATELY, with
|
|
82
|
+
// no threshold: the work is finished and the operator reviewing it is the
|
|
83
|
+
// next move. Its `level` stays 'ok' — a finished VM is never slow or stuck.
|
|
84
|
+
// `needsYouElapsedMs` counts from `doneAt` (the cycle's close time) when the
|
|
85
|
+
// editor reports one, else null.
|
|
81
86
|
// - Missing/unparseable stepStartedAt degrades to stepElapsedMs:null,
|
|
82
87
|
// level:'ok', needsYou:false (older editor, no crash).
|
|
83
88
|
function deriveAttention(opts) {
|
|
@@ -95,6 +100,9 @@ function deriveAttention(opts) {
|
|
|
95
100
|
// its threshold. An 'idle' VM never reaches this branch.
|
|
96
101
|
needsYouElapsedMs = stepElapsed;
|
|
97
102
|
needsYou = stepElapsed != null && stepElapsed >= thresholds.needsYouMs;
|
|
103
|
+
} else if (o.status === 'session-done') {
|
|
104
|
+
needsYou = true;
|
|
105
|
+
needsYouElapsedMs = elapsedMs(o.doneAt, nowMs);
|
|
98
106
|
} else if (o.status === 'building' && stepElapsed != null) {
|
|
99
107
|
if (stepElapsed >= thresholds.stuckMs) level = 'stuck';
|
|
100
108
|
else if (stepElapsed >= thresholds.slowMs) level = 'slow';
|
package/npm/launch-config.js
CHANGED
|
@@ -12,6 +12,7 @@ const { CANONICAL_EDITOR_REPO, EDITOR_VARIANTS, repoBasename, AGENT_ENV_VARS } =
|
|
|
12
12
|
const { normalizeVmSlug } = require("./vm-urls");
|
|
13
13
|
const { normalizeAccessList } = require("./fleet-vm-access");
|
|
14
14
|
const { normalizeRepoUrl } = require("./fleet-env");
|
|
15
|
+
const { posix: posixPath } = require("node:path");
|
|
15
16
|
|
|
16
17
|
/** The build agents a VM's editor terminal can run, as the keys of cloud.js's
|
|
17
18
|
* AGENT_ENV_VARS. Deriving the selectable set from that map keeps it a single
|
|
@@ -1029,11 +1030,58 @@ function credentialShapeCheck(file) {
|
|
|
1029
1030
|
return CREDENTIAL_SHAPE_PATTERNS.map((p) => `grep -Eq '${p}' ${file}`).join(" && ");
|
|
1030
1031
|
}
|
|
1031
1032
|
|
|
1033
|
+
/** Where Claude's sign-in store lives in the container: the editor may relocate
|
|
1034
|
+
* it from the home store into the project. `RELOCATED_STORE_REL` MUST track
|
|
1035
|
+
* Rust's `RELOCATED_STORE_DIR` (crates/control-api/src/claude_store_relocation.rs);
|
|
1036
|
+
* it is resolved against the container's fixed app root `/workspace`. */
|
|
1037
|
+
const RELOCATED_STORE_REL = ".codeyam/agent-home/claude";
|
|
1038
|
+
const RELOCATED_STORE_DIR = `/workspace/${RELOCATED_STORE_REL}`;
|
|
1039
|
+
const HOME_STORE_DIR = "/root/.claude";
|
|
1040
|
+
|
|
1041
|
+
/** Shell step that `cd`s into the directory holding Claude's `name` file —
|
|
1042
|
+
* `.credentials.json` or the onboarding `.claude.json` — so the caller then
|
|
1043
|
+
* names the file by basename. The relocated store wins when it exists; else the
|
|
1044
|
+
* home store. `.claude.json` mirrors the Rust `claude_json_path_at` asymmetry:
|
|
1045
|
+
* relocated → INSIDE the store, home → `/root/.claude.json` (beside the store,
|
|
1046
|
+
* never `/root/.claude/.claude.json`).
|
|
1047
|
+
*
|
|
1048
|
+
* A `cd`, not a `$(…)` path, on purpose: the copy steps run through the VM-host
|
|
1049
|
+
* shell inside a double-quoted `bash -c "…"`, which would expand a command
|
|
1050
|
+
* substitution on the HOST (see `verifyBeforeMoveSink`). This contains no `$`
|
|
1051
|
+
* and no `"`, so it survives every quoting layer and resolves in the container.
|
|
1052
|
+
* The single definition of the store location, shared by the integrity probe,
|
|
1053
|
+
* the donor read and the copy sinks so they can never disagree. `dirs`
|
|
1054
|
+
* overrides the container locations (tests point it at temp dirs). Pure. */
|
|
1055
|
+
function claudeStoreCdStep(name, dirs = {}) {
|
|
1056
|
+
const { relocated = RELOCATED_STORE_DIR, home = HOME_STORE_DIR } = dirs;
|
|
1057
|
+
const homeDir = name === ".claude.json" ? posixPath.dirname(home) : home;
|
|
1058
|
+
return `if [ -d ${relocated} ]; then cd ${relocated}; else cd ${homeDir}; fi`;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/** Shell assignment `STORE=project|home` naming which Claude store is live in
|
|
1062
|
+
* the container, for the dashboard gather. No token is read. `relocated`
|
|
1063
|
+
* overrides the container location for tests. Pure. */
|
|
1064
|
+
function claudeStoreProbe(relocated = RELOCATED_STORE_DIR) {
|
|
1065
|
+
return `STORE=$(test -d ${relocated} && echo project || echo home)`;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** Parse the gather's `STORE` marker: `"project"` / `"home"`, else `null` — an
|
|
1069
|
+
* absent marker (older gather, probe never ran) is unknown, never a fabricated
|
|
1070
|
+
* `home`. Pure. */
|
|
1071
|
+
function parseClaudeStore(marker) {
|
|
1072
|
+
return marker === "project" || marker === "home" ? marker : null;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1032
1075
|
/** Shell assignment `CRED=1|0` for the donor-integrity probe: `1` when the
|
|
1033
|
-
* credential
|
|
1034
|
-
*
|
|
1035
|
-
|
|
1036
|
-
|
|
1076
|
+
* credential passes `credentialShapeCheck`, `0` when it fails or is missing
|
|
1077
|
+
* (either way the copy sink would refuse it). With no `file`, the credential is
|
|
1078
|
+
* resolved via `claudeStoreCdStep` — relocated store first, then home — inside a
|
|
1079
|
+
* subshell, so the caller's cwd is untouched; `dirs` passes through to it. Pure. */
|
|
1080
|
+
function credentialIntegrityProbe(file, dirs) {
|
|
1081
|
+
const check = file
|
|
1082
|
+
? credentialShapeCheck(file)
|
|
1083
|
+
: `${claudeStoreCdStep(".credentials.json", dirs)} && ${credentialShapeCheck(".credentials.json")}`;
|
|
1084
|
+
return `CRED=$( (${check}) 2>/dev/null && echo 1 || echo 0)`;
|
|
1037
1085
|
}
|
|
1038
1086
|
|
|
1039
1087
|
/** Parse the probe's `CRED` marker into the tri-state integrity verdict:
|
|
@@ -1087,13 +1135,17 @@ function donorSkippedLine(donor) {
|
|
|
1087
1135
|
function verifyBeforeMoveSink(
|
|
1088
1136
|
container,
|
|
1089
1137
|
target,
|
|
1090
|
-
{ decode = true, requireCredentialShape = false } = {},
|
|
1138
|
+
{ decode = true, requireCredentialShape = false, cdStep = "" } = {},
|
|
1091
1139
|
) {
|
|
1092
1140
|
const tmp = `${target}.cytmp`;
|
|
1093
1141
|
const read = decode ? "base64 -d" : "cat";
|
|
1094
1142
|
const shape = requireCredentialShape ? ` && ${credentialShapeCheck(tmp)}` : "";
|
|
1143
|
+
// `cdStep` (e.g. `claudeStoreCdStep`) resolves the target's directory in the
|
|
1144
|
+
// container first, so a relative `target` lands in the live store; a failed
|
|
1145
|
+
// `cd` aborts before anything is written.
|
|
1146
|
+
const cd = cdStep ? `${cdStep} || exit 1; ` : "";
|
|
1095
1147
|
const script =
|
|
1096
|
-
|
|
1148
|
+
`${cd}umask 077; ${read} > ${tmp}; ` +
|
|
1097
1149
|
`if [ -s ${tmp} ]${shape}; then mv ${tmp} ${target} && chmod 600 ${target}; ` +
|
|
1098
1150
|
`else rm -f ${tmp}; exit 1; fi`;
|
|
1099
1151
|
return `docker exec -i ${container} bash -c "${script}"`;
|
|
@@ -1108,8 +1160,8 @@ function verifyBeforeMoveSink(
|
|
|
1108
1160
|
*
|
|
1109
1161
|
* Donor mode (the default) copies, container→container from the donor VM:
|
|
1110
1162
|
* 1. host-file git credentials → `$HOME/.codeyam-cloud-git-credentials`
|
|
1111
|
-
* 2. claude `.credentials.json` →
|
|
1112
|
-
* 3. claude `.claude.json` →
|
|
1163
|
+
* 2. claude `.credentials.json` → the target's live store (`claudeStoreCdStep`)
|
|
1164
|
+
* 3. claude `.claude.json` → the target's live onboarding dotfile
|
|
1113
1165
|
* Step 3 (onboarding state — `hasCompletedOnboarding` + sibling keys) is what
|
|
1114
1166
|
* keeps the first interactive `claude` TUI from popping the welcome "Select
|
|
1115
1167
|
* login method" menu; without it the non-interactive `claude --print` probe
|
|
@@ -1134,19 +1186,27 @@ function buildCredsSteps({ mode, refInst = "", inst, container, gcloud, zone, n,
|
|
|
1134
1186
|
// non-empty-but-corrupt donor must not clobber a working target. The
|
|
1135
1187
|
// `.claude.json` (onboarding-state) copy has a different shape, so it keeps
|
|
1136
1188
|
// the plain non-empty guard.
|
|
1137
|
-
|
|
1189
|
+
// Both ends follow Claude's store wherever the editor put it (relocated into
|
|
1190
|
+
// the project, or the home store): the donor is read from its live store and
|
|
1191
|
+
// the target written into ITS live store, via the one `claudeStoreCdStep`.
|
|
1192
|
+
const claudeSink = verifyBeforeMoveSink(container, ".credentials.json", {
|
|
1138
1193
|
requireCredentialShape: true,
|
|
1194
|
+
cdStep: claudeStoreCdStep(".credentials.json"),
|
|
1195
|
+
});
|
|
1196
|
+
const claudeJsonSink = verifyBeforeMoveSink(container, ".claude.json", {
|
|
1197
|
+
cdStep: claudeStoreCdStep(".claude.json"),
|
|
1139
1198
|
});
|
|
1140
|
-
const
|
|
1199
|
+
const donorRead = (name) =>
|
|
1200
|
+
`docker exec ${container} bash -c "${claudeStoreCdStep(name)} && base64 ${name}"`;
|
|
1141
1201
|
const loginHint = `run: docker exec -it ${container} claude /login on ${inst}`;
|
|
1142
1202
|
if (mode === "donor") {
|
|
1143
1203
|
return [
|
|
1144
1204
|
`echo "copying git creds from ${refInst}..."`,
|
|
1145
1205
|
`${gcloud} compute ssh ${refInst} --tunnel-through-iap --zone=${zone} --command='cat $HOME/.codeyam-cloud-git-credentials' 2>/dev/null | ${gcloud} compute ssh ${inst} --tunnel-through-iap --zone=${zone} --command='umask 077; cat > $HOME/.codeyam-cloud-git-credentials' 2>/dev/null || echo "CY_DEGRADED git creds copy failed on VM-${n} — re-copy creds from a live donor (the agent can't git pull/push until then)"`,
|
|
1146
1206
|
`echo "copying claude auth from ${refInst}${acctNote} (container->container)..."`,
|
|
1147
|
-
`${gcloud} compute ssh ${refInst} --tunnel-through-iap --zone=${zone} --command
|
|
1207
|
+
`${gcloud} compute ssh ${refInst} --tunnel-through-iap --zone=${zone} --command=${_shq(donorRead(".credentials.json"))} 2>/dev/null | ${gcloud} compute ssh ${inst} --tunnel-through-iap --zone=${zone} --command=${_shq(claudeSink)} 2>/dev/null && echo "claude auth copied" || echo "CY_DEGRADED claude-auth-not-seeded: claude auth copy failed on VM-${n} (empty/unreadable/malformed donor — existing creds left untouched, not overwritten with a corrupt credential) — ${loginHint}"`,
|
|
1148
1208
|
`echo "copying claude.json (onboarding state) from ${refInst} (container->container)..."`,
|
|
1149
|
-
`${gcloud} compute ssh ${refInst} --tunnel-through-iap --zone=${zone} --command
|
|
1209
|
+
`${gcloud} compute ssh ${refInst} --tunnel-through-iap --zone=${zone} --command=${_shq(donorRead(".claude.json"))} 2>/dev/null | ${gcloud} compute ssh ${inst} --tunnel-through-iap --zone=${zone} --command=${_shq(claudeJsonSink)} 2>/dev/null && echo "claude.json copied" || echo "CY_DEGRADED claude-auth-not-seeded: claude.json copy failed on VM-${n} — first interactive claude launch may show 'Select login method' menu; ${loginHint} once to complete onboarding"`,
|
|
1150
1210
|
];
|
|
1151
1211
|
}
|
|
1152
1212
|
if (mode === "host-stash") {
|
|
@@ -1532,6 +1592,11 @@ module.exports = {
|
|
|
1532
1592
|
CREDENTIAL_SHAPE_PATTERNS,
|
|
1533
1593
|
credentialShapeCheck,
|
|
1534
1594
|
credentialIntegrityProbe,
|
|
1595
|
+
RELOCATED_STORE_REL,
|
|
1596
|
+
HOME_STORE_DIR,
|
|
1597
|
+
claudeStoreCdStep,
|
|
1598
|
+
claudeStoreProbe,
|
|
1599
|
+
parseClaudeStore,
|
|
1535
1600
|
parseCredentialIntact,
|
|
1536
1601
|
donorSkippedLine,
|
|
1537
1602
|
buildClaudeJsonPreserveSteps,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeyam-editor/codeyam-editor",
|
|
3
|
-
"version": "0.1.12-staging.
|
|
3
|
+
"version": "0.1.12-staging.gf52ff7a",
|
|
4
4
|
"description": "Language-agnostic managed execution sandbox for scenario-driven development",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codeyam-editor": "./npm/cli.js"
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"playwright": "^1.58.2"
|
|
10
10
|
},
|
|
11
11
|
"optionalDependencies": {
|
|
12
|
-
"@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.12-staging.
|
|
13
|
-
"@codeyam-editor/codeyam-editor-darwin-x64": "0.1.12-staging.
|
|
14
|
-
"@codeyam-editor/codeyam-editor-linux-x64": "0.1.12-staging.
|
|
15
|
-
"@codeyam-editor/codeyam-editor-win32-x64": "0.1.12-staging.
|
|
12
|
+
"@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.12-staging.gf52ff7a",
|
|
13
|
+
"@codeyam-editor/codeyam-editor-darwin-x64": "0.1.12-staging.gf52ff7a",
|
|
14
|
+
"@codeyam-editor/codeyam-editor-linux-x64": "0.1.12-staging.gf52ff7a",
|
|
15
|
+
"@codeyam-editor/codeyam-editor-win32-x64": "0.1.12-staging.gf52ff7a"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"codeyam",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{j as e}from"./markdown-C9tbsVHX.js";import{b as a}from"./react-nrLBr15I.js";import{u as k}from"./useEvents-S3Qk1Y_d.js";import{k as E}from"./index-
|
|
1
|
+
import{j as e}from"./markdown-C9tbsVHX.js";import{b as a}from"./react-nrLBr15I.js";import{u as k}from"./useEvents-S3Qk1Y_d.js";import{k as E}from"./index-maa9OUlU.js";function P({connected:t}){return e.jsx("span",{className:`inline-block rounded-full px-2 py-0.5 text-xs font-medium text-[var(--text-primary)] ${t?"bg-[var(--accent-green)]":"bg-[var(--accent-red)]"}`,children:t?"connected":"reconnecting…"})}const re=Object.freeze(Object.defineProperty({__proto__:null,ConnectionBadge:P},Symbol.toStringTag,{value:"Module"}));function N(){const[t,r]=a.useState(null),[o,s]=a.useState(!0),{events:l}=k(),n=a.useCallback(async()=>{try{const c=await(await fetch("/api/inspector/status")).json();r(c)}catch{}finally{s(!1)}},[]);return a.useEffect(()=>{n()},[n]),a.useEffect(()=>{const i=l[l.length-1];i&&i.type==="scenario_switch"&&n()},[l,n]),{scenario:(t==null?void 0:t.scenario)??null,previewScenario:(t==null?void 0:t.previewScenario)??null,routes:(t==null?void 0:t.routes)??[],routeCount:(t==null?void 0:t.routeCount)??0,loading:o,refetch:n}}function v({title:t}){return e.jsx("h2",{style:{fontSize:14,fontWeight:600,color:"#f0f6fc",margin:"0 0 8px 0",borderBottom:"1px solid #30363d",paddingBottom:8},children:t})}function f({color:t,children:r}){return e.jsx("span",{style:{background:`${t}22`,color:t,border:`1px solid ${t}55`,borderRadius:4,padding:"2px 8px",fontSize:11,fontWeight:600,letterSpacing:"0.5px",whiteSpace:"nowrap"},children:r})}const $={GET:"#D7FF63",POST:"#238636",PUT:"#d29922",DELETE:"#da3633",PATCH:"#bc8cff"};function O({method:t}){const r=$[t.toUpperCase()]??"#8b949e";return e.jsx("span",{style:{color:r,fontWeight:600,fontSize:11,minWidth:48,display:"inline-block"},children:t.toUpperCase()})}function C({color:t,children:r}){return e.jsx("div",{style:{padding:"12px 16px",borderRadius:6,border:`1px solid ${t}44`,background:`${t}11`,color:t},children:r})}const u={padding:"8px 12px",textAlign:"center",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px"},h={padding:"6px 12px"},S={background:"#161b22",border:"1px solid #30363d",borderRadius:6,color:"#c9d1d9",padding:"6px 12px",fontSize:13,fontFamily:"inherit"},R={...S,appearance:"none",paddingRight:24,backgroundImage:`url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238b949e' d='M6 8L1 3h10z'/%3E%3C/svg%3E")`,backgroundRepeat:"no-repeat",backgroundPosition:"right 8px center"},oe=Object.freeze(Object.defineProperty({__proto__:null,Badge:f,MethodBadge:O,SectionHeader:v,StatusBox:C,inputStyle:S,selectStyle:R,tdStyle:h,thStyle:u},Symbol.toStringTag,{value:"Module"}));function z({routes:t}){return e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d"},children:[e.jsx("th",{style:u,children:"Method"}),e.jsx("th",{style:{...u,textAlign:"left"},children:"Pattern"}),e.jsx("th",{style:u,children:"Status"})]})}),e.jsx("tbody",{children:t.map((r,o)=>e.jsxs("tr",{style:{borderBottom:"1px solid #21262d"},children:[e.jsx("td",{style:{...h,textAlign:"center"},children:e.jsx(O,{method:r.method})}),e.jsx("td",{style:h,children:r.pattern}),e.jsx("td",{style:{...h,textAlign:"center",color:"#8b949e"},children:r.status})]},o))})]})}const se=Object.freeze(Object.defineProperty({__proto__:null,InspectorRouteTable:z},Symbol.toStringTag,{value:"Module"}));function A(){const{scenario:t,previewScenario:r,routes:o,routeCount:s,loading:l}=N();return e.jsxs("section",{children:[e.jsx(v,{title:"Active Scenario"}),l?e.jsx(C,{color:"#8b949e",children:"Loading..."}):t?e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[e.jsx(f,{color:"#238636",children:"ACTIVE"}),e.jsx("span",{style:{color:"#f0f6fc",fontWeight:500},children:t.name}),e.jsxs("span",{style:{color:"#8b949e"},children:["(",t.slug,")"]})]}),e.jsxs("div",{style:{color:"#8b949e",fontSize:12},children:[s," mock route",s!==1?"s":""," loaded"]}),o.length>0&&e.jsx(z,{routes:o})]}):r?e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[e.jsx(f,{color:"#D7FF63",children:"PREVIEW"}),e.jsx("span",{style:{color:"#f0f6fc",fontWeight:500},children:r.name}),e.jsxs("span",{style:{color:"#8b949e"},children:["(",r.slug,")"]})]}),e.jsx("div",{style:{color:"#8b949e",fontSize:12},children:"Component preview — no HTTP mocks active"})]}):e.jsx(C,{color:"#d29922",children:"No scenario active — all requests pass through to upstream"})]})}const ne=Object.freeze(Object.defineProperty({__proto__:null,InspectorScenarioSection:A},Symbol.toStringTag,{value:"Module"}));function F({textUrlFilter:t,onTextUrlFilterChange:r,methodFilter:o,onMethodFilterChange:s,sourceFilter:l,onSourceFilterChange:n}){return e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12,alignItems:"center"},children:[e.jsx("input",{type:"text",placeholder:"Filter by URL...",value:t,onChange:i=>r(i.target.value),style:{...S,flex:1,minWidth:0}}),e.jsxs("select",{value:o,onChange:i=>s(i.target.value),style:R,children:[e.jsx("option",{value:"ALL",children:"All Methods"}),e.jsx("option",{value:"GET",children:"GET"}),e.jsx("option",{value:"POST",children:"POST"}),e.jsx("option",{value:"PUT",children:"PUT"}),e.jsx("option",{value:"DELETE",children:"DELETE"}),e.jsx("option",{value:"PATCH",children:"PATCH"})]}),e.jsxs("select",{value:l,onChange:i=>n(i.target.value),style:R,children:[e.jsx("option",{value:"ALL",children:"All Sources"}),e.jsx("option",{value:"MOCKED",children:"Mocked"}),e.jsx("option",{value:"PASSTHROUGH",children:"Passthrough"})]})]})}const ie=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficFilterBar:F},Symbol.toStringTag,{value:"Module"})),G={color:"#8b949e",fontSize:11,fontWeight:600,textTransform:"uppercase",letterSpacing:"0.5px",marginBottom:4},M={background:"#161b22",border:"1px solid #30363d",borderRadius:4,padding:"8px 12px",margin:0,fontSize:11,color:"#c9d1d9",fontFamily:"monospace",maxHeight:200,overflow:"auto",whiteSpace:"pre-wrap",wordBreak:"break-all"};function _({label:t,children:r}){return e.jsxs("div",{style:{marginBottom:12},children:[e.jsx("div",{style:G,children:t}),r]})}function B({event:t}){let r=[];try{const n=new URL(t.url,"http://localhost");r=Array.from(n.searchParams.entries())}catch{}const o=t.requestHeaders&&Object.keys(t.requestHeaders).length>0,s=t.requestBody!=null,l=t.responseBody!=null;return e.jsxs("div",{style:{background:"#0d1117",padding:"12px 16px",borderTop:"1px solid #30363d"},children:[r.length>0&&e.jsx(_,{label:"Query Parameters",children:e.jsx("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"2px 12px",fontSize:12},children:r.map(([n,i],c)=>e.jsxs("div",{style:{display:"contents"},children:[e.jsx("span",{style:{color:"#D7FF63",fontFamily:"monospace"},children:n}),e.jsx("span",{style:{color:"#c9d1d9",fontFamily:"monospace"},children:i})]},c))})}),o&&e.jsx(_,{label:"Request Headers",children:e.jsx("pre",{style:M,children:Object.entries(t.requestHeaders).map(([n,i])=>`${n}: ${i}`).join(`
|
|
2
2
|
`)})}),s&&e.jsx(_,{label:"Request Body",children:e.jsx("pre",{style:M,children:typeof t.requestBody=="string"?t.requestBody:JSON.stringify(t.requestBody,null,2)})}),l&&e.jsx(_,{label:"Response Body",children:e.jsx("pre",{style:M,children:typeof t.responseBody=="string"?t.responseBody:JSON.stringify(t.responseBody,null,2)})}),r.length===0&&!o&&!s&&!l&&e.jsx("div",{style:{color:"#8b949e",fontSize:12},children:"No additional details available for this request"})]})}const le=Object.freeze(Object.defineProperty({__proto__:null,DetailSection:_,InspectorTrafficDetail:B},Symbol.toStringTag,{value:"Module"}));function L({event:t}){const[r,o]=a.useState(!1),s=new Date(t.timestampMs).toLocaleTimeString(),l=t.mocked;return e.jsxs(e.Fragment,{children:[e.jsxs("tr",{onClick:()=>o(!r),style:{borderBottom:"1px solid #21262d",background:l?"transparent":"rgba(210, 153, 34, 0.05)",cursor:"pointer"},children:[e.jsxs("td",{style:{...h,color:"#8b949e",textAlign:"center"},children:[e.jsx("span",{style:{display:"inline-block",width:12,marginRight:4,fontSize:10,color:"#8b949e",transition:"transform 0.15s",transform:r?"rotate(90deg)":"rotate(0deg)"},children:"▶"}),s]}),e.jsx("td",{style:{...h,textAlign:"center"},children:e.jsx(O,{method:t.method})}),e.jsx("td",{style:{...h,maxWidth:400,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:t.url}),e.jsx("td",{style:{...h,textAlign:"center",color:"#8b949e"},children:t.status??"---"}),e.jsxs("td",{style:{...h,textAlign:"center",color:"#8b949e"},children:[t.durationMs,"ms"]}),e.jsx("td",{style:{...h,textAlign:"center"},children:l?t.mockSource==="default"?e.jsx(f,{color:"#1f6feb",children:"MOCKED (default)"}):e.jsx(f,{color:"#238636",children:"MOCKED (scenario)"}):e.jsx(f,{color:"#d29922",children:"PASSTHROUGH"})})]}),r&&e.jsx("tr",{children:e.jsx("td",{colSpan:6,style:{padding:0},children:e.jsx(B,{event:t})})})]})}const ae=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficRow:L},Symbol.toStringTag,{value:"Module"}));function D({events:t}){return e.jsx("div",{style:{maxHeight:300,overflowY:"auto",border:"1px solid #30363d",borderRadius:6},children:e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d",position:"sticky",top:0,background:"#0d1117"},children:[e.jsx("th",{style:u,children:"Time"}),e.jsx("th",{style:u,children:"Method"}),e.jsx("th",{style:{...u,textAlign:"left"},children:"URL"}),e.jsx("th",{style:u,children:"Status"}),e.jsx("th",{style:u,children:"Duration"}),e.jsx("th",{style:u,children:"Source"})]})}),e.jsx("tbody",{children:t.map(r=>e.jsx(L,{event:r},r.id))})]})})}const ce=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficTable:D},Symbol.toStringTag,{value:"Module"}));function K(t){if(t===void 0)return"checked";if(t==="checked")return"excluded"}function I({chips:t,chipStates:r,onChipStatesChange:o}){if(t.length===0)return null;const s=n=>{const i=new Map(r),c=K(i.get(n));c===void 0?i.delete(n):i.set(n,c),o(i)},l=()=>{o(new Map)};return e.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:6,marginBottom:12},children:[r.size>0&&e.jsx("button",{onClick:l,style:{background:"#da363322",color:"#da3633",border:"1px solid #da363355",borderRadius:4,padding:"2px 8px",fontSize:11,fontWeight:600,cursor:"pointer",fontFamily:"inherit"},children:"Clear all"}),t.map(({path:n,count:i})=>{const c=r.get(n),d=c==="checked",g=c==="excluded";let m="#30363d",b="#8b949e",T="#30363d",y="";return d?(m="#D7FF6322",b="#D7FF63",T="#D7FF6355",y="✓ "):g&&(m="#da363322",b="#da3633",T="#da363355",y="✗ "),e.jsxs("button",{onClick:()=>s(n),style:{background:m,color:b,border:`1px solid ${T}`,borderRadius:4,padding:"2px 8px",fontSize:11,fontFamily:"monospace",cursor:"pointer",whiteSpace:"nowrap"},children:[y,n,e.jsxs("span",{style:{marginLeft:4,opacity:.6,fontFamily:"inherit"},children:["(",i,")"]})]},n)})]})}const de=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficUrlChips:I},Symbol.toStringTag,{value:"Module"}));function H(){const{events:t}=k(),[r,o]=a.useState(""),[s,l]=a.useState(()=>new Map),[n,i]=a.useState("ALL"),[c,d]=a.useState("ALL"),g=t.filter(p=>p.type==="http_request"),m=a.useMemo(()=>{const p=new Map;for(const x of g){const j=E(x.url);p.set(j,(p.get(j)||0)+1)}return Array.from(p.entries()).sort((x,j)=>j[1]-x[1]).slice(0,10).map(([x,j])=>({path:x,count:j}))},[g]),b=a.useMemo(()=>Array.from(s.values()).some(p=>p==="checked"),[s]),y=g.filter(p=>{if(r&&!p.url.toLowerCase().includes(r.toLowerCase()))return!1;const x=E(p.url);return!(s.get(x)==="excluded"||b&&s.get(x)!=="checked"||n!=="ALL"&&p.method.toUpperCase()!==n||c==="MOCKED"&&!p.mocked||c==="PASSTHROUGH"&&p.mocked)}).slice(-50).reverse();return e.jsxs("section",{children:[e.jsx(v,{title:"Live Traffic"}),e.jsx(F,{textUrlFilter:r,onTextUrlFilterChange:o,methodFilter:n,onMethodFilterChange:i,sourceFilter:c,onSourceFilterChange:p=>d(p)}),e.jsx(I,{chips:m,chipStates:s,onChipStatesChange:l}),y.length===0?e.jsx(C,{color:"#8b949e",children:g.length>0?"No matching traffic — try adjusting the filters":"No HTTP traffic yet — requests will appear here in real-time"}):e.jsx(D,{events:y})]})}const pe=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficSection:H},Symbol.toStringTag,{value:"Module"}));function J(){const[t,r]=a.useState(null),[o,s]=a.useState(!1);return{explain:a.useCallback(async(n,i)=>{s(!0);try{const d=await(await fetch("/api/inspector/explain",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:n,url:i})})).json();r(d)}catch{}finally{s(!1)}},[]),result:t,loading:o}}function U({result:t}){return e.jsxs("div",{style:{border:"1px solid #30363d",borderRadius:6,padding:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,marginBottom:12},children:[e.jsxs("span",{style:{color:"#f0f6fc",fontWeight:500},children:[t.method," ",t.url]}),t.wouldMatch?e.jsx(f,{color:"#238636",children:"WOULD MATCH"}):e.jsx(f,{color:"#da3633",children:"NO MATCH"})]}),t.attempts.length===0?e.jsx("div",{style:{color:"#8b949e"},children:"No routes loaded to match against"}):e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:6},children:t.attempts.map((r,o)=>e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,padding:"4px 8px",borderRadius:4,background:r.failure?"transparent":"rgba(35, 134, 54, 0.1)"},children:[e.jsx("span",{style:{color:r.failure?"#da3633":"#238636",fontWeight:600,width:16},children:r.failure?"x":"v"}),e.jsx(O,{method:r.method}),e.jsx("span",{children:r.pattern}),r.failure&&e.jsx("span",{style:{color:"#8b949e",fontSize:11},children:r.failure.reason==="methodMismatch"?`method: expected ${r.failure.expected}, got ${r.failure.actual}`:`path "${r.failure.path}" does not match "${r.failure.pattern}"`})]},o))})]})}const ue=Object.freeze(Object.defineProperty({__proto__:null,InspectorExplainerResult:U},Symbol.toStringTag,{value:"Module"}));function W(){const{explain:t,result:r,loading:o}=J(),[s,l]=a.useState("GET"),[n,i]=a.useState(""),c=d=>{d.preventDefault(),n.trim()&&t(s,n.trim())};return e.jsxs("section",{children:[e.jsx(v,{title:"Match Explainer"}),e.jsxs("form",{onSubmit:c,style:{display:"flex",gap:8,marginBottom:12},children:[e.jsx("select",{value:s,onChange:d=>l(d.target.value),style:{...S,width:100},children:["GET","POST","PUT","DELETE","PATCH"].map(d=>e.jsx("option",{value:d,children:d},d))}),e.jsx("input",{type:"text",value:n,onChange:d=>i(d.target.value),placeholder:"/api/users",style:{...S,flex:1}}),e.jsx("button",{type:"submit",disabled:o||!n.trim(),style:{background:"#238636",color:"#fff",border:"none",borderRadius:6,padding:"6px 16px",cursor:"pointer",fontSize:12,fontWeight:500,opacity:o||!n.trim()?.5:1},children:o?"Checking...":"Explain"})]}),r&&e.jsx(U,{result:r})]})}const he=Object.freeze(Object.defineProperty({__proto__:null,InspectorExplainerSection:W},Symbol.toStringTag,{value:"Module"})),w={"/api/health":"Health check — confirms the proxy and mock engine are running","/api/events":"SSE stream of real-time events (traffic, errors) powering this Inspector","/api/config":"Returns current CodeYam configuration for the project","/api/editor-dev-server":"Polls/controls the dev server status (start, stop, restart)","/api/inspector/status":"Returns active scenario and loaded mock routes","/api/inspector/explain":"Tests which mock route would match a given method + URL","/api/scenarios":"Lists all defined scenarios with metadata","/api/tests":"Returns the AI-maintained test registry joined with per-test-evidence, grouped by file","/api/test-results":"Returns latest test execution results","/api/screenshots/*":"Serves captured scenario screenshots","/api/session-info":"Returns current editor session metadata","/api/data-structure":"Returns the app's data structure definition","/api/dependency-graph":"Returns the project's component dependency graph","/api/app-route-entities":"Lists app routes and their associated entities","/api/glossary":"Returns the project's component glossary","/api/step":"Returns current editor workflow step","/api/editor-commands":"Lists available editor commands"};function V(t){if(w[t])return w[t];for(const[r,o]of Object.entries(w))if(r.endsWith("/*")){const s=r.slice(0,-1);if(t.startsWith(s))return o}}function Y(t){const r=new Set([...t,...Object.keys(w)]),o=[];for(const s of Array.from(r).sort()){if(s.endsWith("/*"))continue;const l=V(s);o.push({path:s,description:l??"Application endpoint"})}return o}function q(){const{events:t}=k(),r=a.useMemo(()=>{const o=[];for(const s of t)s.type==="http_request"&&o.push(E(s.url));return Y(o)},[t]);return r.length===0?null:e.jsxs("section",{children:[e.jsx(v,{title:"URL Glossary"}),e.jsx("div",{style:{border:"1px solid #30363d",borderRadius:6,overflow:"hidden"},children:e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d",background:"#161b22"},children:[e.jsx("th",{style:{padding:"6px 12px",textAlign:"left",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px",width:"30%"},children:"Path"}),e.jsx("th",{style:{padding:"6px 12px",textAlign:"left",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px"},children:"Description"})]})}),e.jsx("tbody",{children:r.map(({path:o,description:s})=>e.jsxs("tr",{style:{borderBottom:"1px solid #21262d"},children:[e.jsx("td",{style:{padding:"4px 12px",fontFamily:"monospace",color:"#D7FF63",fontSize:11},children:o}),e.jsx("td",{style:{padding:"4px 12px",color:"#8b949e"},children:s})]},o))})]})})]})}const xe=Object.freeze(Object.defineProperty({__proto__:null,InspectorUrlGlossary:q},Symbol.toStringTag,{value:"Module"}));function Q(){const{connected:t}=k();return e.jsxs("div",{style:{background:"#0d1117",color:"#c9d1d9",minHeight:"100vh",fontFamily:"'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, monospace",fontSize:13,padding:24,display:"flex",flexDirection:"column",gap:24},children:[e.jsxs("header",{style:{display:"flex",alignItems:"center",gap:12},children:[e.jsx("h1",{style:{fontSize:18,fontWeight:600,color:"#f0f6fc",margin:0},children:"Container Inspector"}),e.jsx("span",{style:{color:"#8b949e",fontSize:12},children:"Read-only view of proxy and mock engine state"}),e.jsx(P,{connected:t})]}),e.jsx(A,{}),e.jsx(H,{}),e.jsx(W,{}),e.jsx(q,{})]})}const fe=Object.freeze(Object.defineProperty({__proto__:null,ContainerInspector:Q},Symbol.toStringTag,{value:"Module"}));export{f as B,Q as C,_ as D,L as I,O as M,v as S,xe as _,C as a,q as b,I as c,D as d,F as e,B as f,U as g,W as h,H as i,z as j,A as k,P as l,de as m,ce as n,pe as o,ae as p,ie as q,le as r,ne as s,u as t,se as u,oe as v,he as w,ue as x,fe as y,re as z};
|