Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
1547f6c
feat(live): manual text-edit panel + Astro inject + stale-lockfile reap
abdulwahabone May 15, 2026
e9e6534
feat(live): inline contenteditable text editing
abdulwahabone May 16, 2026
ff750f4
fix(live): hide annotation overlay during inline edit
abdulwahabone May 16, 2026
64b0632
feat(live): edit content badge mode with batched saves
abdulwahabone May 16, 2026
d423824
fix(live): use row.el.tagName for tag in applyEditing op
abdulwahabone May 16, 2026
14a4b5c
feat(live): Edit content badge styling + auto-focus + separate buttons
abdulwahabone May 16, 2026
e31f489
feat(live): Subtle button UI + cursor positioning + better copy
abdulwahabone May 16, 2026
44ee7eb
fix(live): Use site design system colors for edit badge buttons
abdulwahabone May 16, 2026
68608f1
fix(live): Match slop-callout style for edit badge buttons
abdulwahabone May 16, 2026
873d5b9
fix(live): Pill-shaped edit badge buttons, 2px padding, no uppercase
abdulwahabone May 16, 2026
b4680f0
fix(live): Cancel button uses mist border + ash text
abdulwahabone May 16, 2026
489f893
fix(live): Remove blue focus outline from contenteditable elements in…
abdulwahabone May 16, 2026
e4e1616
feat(live): Decouple manual edits from agent/poll pipeline
abdulwahabone May 16, 2026
4638a47
feat(live): Stash manual edits server-side; commit via AI on request
abdulwahabone May 16, 2026
2e5bfdf
feat(live): Staged-edits pill becomes an "Apply" button
abdulwahabone May 16, 2026
3127ff2
chore(live): gitignore pending-manual-edits.json runtime buffer
abdulwahabone May 16, 2026
c42c4e6
chore: drop stray site/ test edits from PR
abdulwahabone May 16, 2026
9cb7c25
feat(live): Pill label reads "Apply N staged"
abdulwahabone May 16, 2026
a80c563
fix(live): Manual edit ops use the leaf element's locator, not parent's
abdulwahabone May 16, 2026
d28fa17
fix(live): Climb to nearest classed ancestor when leaf has no locator
abdulwahabone May 16, 2026
dbaa5d5
feat(live): Make mixed-content paragraphs editable
abdulwahabone May 16, 2026
3857a12
fix(live): Address Cursor Bugbot findings (CB-2 through CB-6)
abdulwahabone May 16, 2026
f61a555
fix(live): A3+A4 data-integrity guards, A6 test coverage
abdulwahabone May 16, 2026
b83ea1c
chore: drop .claude/pr-review.md from PR
abdulwahabone May 16, 2026
0b455e5
chore: drop stray site/ test edits from PR (round 2)
abdulwahabone May 16, 2026
a42b400
feat(live): Disable Edit badge while variants are generating
abdulwahabone May 16, 2026
463fe9e
fix(live): live-wrap refuses without --page-url when buffer has pendi…
abdulwahabone May 16, 2026
6af730e
change back
abdulwahabone May 16, 2026
c84d64a
chore: drop stray site/ test edits from PR (round 3)
abdulwahabone May 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 59 additions & 7 deletions .agents/skills/impeccable/reference/live.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ LOOP:
node .agents/skills/impeccable/scripts/live-poll.mjs # default long timeout; no --timeout=
Read JSON; dispatch on "type"

"generate" → Handle Generate; reply done; LOOP
"accept" → Handle Accept; complete carbonize cleanup if required; LOOP
"discard" → Handle Discard; LOOP
"prefetch" → Handle Prefetch; LOOP
"timeout" → LOOP
"exit" → break → Cleanup
"generate" → Handle Generate; reply done; LOOP
"accept" → Handle Accept; complete carbonize cleanup if required; LOOP
"discard" → Handle Discard; LOOP
"prefetch" → Handle Prefetch; LOOP
"timeout" → LOOP
"exit" → break → Cleanup
```

## Recovery commands
Expand Down Expand Up @@ -93,7 +93,7 @@ Reading annotations precisely:
### 2. Wrap the element

```bash
node .agents/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET"
node .agents/skills/impeccable/scripts/live-wrap.mjs --id EVENT_ID --count EVENT_COUNT --element-id "ELEMENT_ID" --classes "class1,class2" --tag "div" --text "TEXT_SNIPPET" --page-url "/path"
```

Flag mapping. Keep them separate, don't collapse into `--query`:
Expand All @@ -102,6 +102,7 @@ Flag mapping. Keep them separate, don't collapse into `--query`:
- `--classes` ← `event.element.classes` joined with commas
- `--tag` ← `event.element.tagName`
- `--text` ← first ~80 chars of `event.element.textContent` (trim, single-line). **Pass this every call.** When the picked element shares classes + tag with sibling components (a list of `<Card>`s, repeating sections), this is what disambiguates which branch in source to wrap. Without it, wrap silently lands on the first match and may rewrite the wrong element.
- `--page-url` ← `event.pageUrl`. **Required when the manual-edit buffer has any pending entries** (`live-wrap.mjs` will exit with `missing_page_url_with_pending_edits` otherwise). Scopes the buffer-aware "original" content step to edits made on this page so the wrap block's `data-impeccable-variant="original"` reflects the user's staged DOM, not the un-edited source. Pass it every call: the buffer state can change between events, and forgetting it produces variants that look correct in source but ignore the user's pending changes.

The helper searches ID first, then classes, then tag + class combo. If `event.pageUrl` implies the file (e.g. `/` is usually `index.html`), pass `--file PATH` to skip the search. `--query` is a fallback for raw text search only; do not use it for normal element lookups.

Expand Down Expand Up @@ -394,6 +395,57 @@ Then remove the temporary wrapper from the served file if it's still there.

Remove the wrapper you inserted in Step 2. Nothing else to do.

## Manual edits: stashed server-side; commit on user request

When the user clicks Save in the live overlay, the browser POSTs to `/manual-edit-stash`. The server appends to `.impeccable/live/pending-manual-edits.json`. **No source file is touched. No HMR refresh.** The event is never enqueued and never reaches the poll loop. You do not see manual-edit traffic until the user explicitly asks you to commit.

The user's edited DOM state becomes the "current truth" for downstream operations. `live-wrap.mjs` is buffer-aware: when it wraps an element that has a pending manual edit, the wrap block's `data-impeccable-variant="original"` content reflects the edited text, not the raw source. `live-accept.mjs` scrubs matching buffer entries after a successful accept (the accept embodies the manual edit, so the pending op is consumed, not lost). Variant **discard** does NOT touch the buffer; the manual edit is preserved.

### When to commit

Run `live-commit-manual-edits.mjs` ONLY when the user clearly asks to commit, apply, or flush pending manual edits. Examples:

- "commit my edits"
- "apply the manual edits"
- "flush pending"
- "save those changes" (when context makes it about the manual edits, not unrelated files)

Do NOT trigger on generic "save" mentions about unrelated files or work.

```
node .agents/skills/impeccable/scripts/live-commit-manual-edits.mjs
```

Optional `--page-url=<url>` to scope.

Output JSON: `{ applied, failed, files, cleared, reason? }`.

- `cleared: true`: all ops succeeded. Acknowledge in one short line: "Committed N edits across M files."
- `cleared: false, reason: "no_pending_edits"`: buffer was empty. Say "Nothing to commit."
- `cleared: false` with failed entries: surface the failures and reasons. Failed ops stay in the buffer; user can fix source manually and retry, or discard. Common reasons:
- `text_not_in_source`: `originalText` not found in the matched element's source range.
- `text_ambiguous_in_block`: `originalText` appears more than once in the matched element block; the locator can't tell which leaf to update. Ask the user to rephrase one of the duplicates, then retry.
- `element_ambiguous` / `element_not_found`: locator failed.
- `invalid_chars_in_newText`: `newText` contained `<`, `>`, `{`, `}`, or a backtick. The manual-edit flow is plain-text only. If the user wants to insert markup, do it yourself with the Edit tool against the source file.

### When to discard

Run `live-discard-manual-edits.mjs` when the user asks to discard, throw away, or clear pending manual edits. Examples:

- "discard the pending edits"
- "throw away my unsaved manual edits"
- "clear the staging buffer"

```
node .agents/skills/impeccable/scripts/live-discard-manual-edits.mjs
```

Optional `--page-url=<url>` to scope. Output: `{ discarded, totalCount }`.

### Do NOT auto-commit

Do not run commit on session start, on idle, or as a side effect of any other event. Only on explicit user request. The buffer can hold edits across sessions indefinitely; that is intended.

## Handle `accept`

Event: `{id, variantId, _acceptResult, _completionAck}`. The poll script already ran `live-accept.mjs` to handle the file operation deterministically, then acknowledged event delivery to the helper. The browser DOM is already updated.
Expand Down
11 changes: 10 additions & 1 deletion .agents/skills/impeccable/scripts/impeccable-paths.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,16 @@ export function getLegacyLiveServerPath(cwd = process.cwd()) {
export function readLiveServerInfo(cwd = process.cwd()) {
for (const filePath of [getLiveServerPath(cwd), getLegacyLiveServerPath(cwd)]) {
try {
return { info: JSON.parse(fs.readFileSync(filePath, 'utf-8')), path: filePath };
const info = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
if (info && typeof info.pid === 'number') {
try {
process.kill(info.pid, 0);
} catch {
try { fs.unlinkSync(filePath); } catch {}
continue;
}
}
return { info, path: filePath };
} catch {
/* try next */
}
Expand Down
40 changes: 39 additions & 1 deletion .agents/skills/impeccable/scripts/live-accept.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import fs from 'node:fs';
import path from 'node:path';
import { isGeneratedFile } from './is-generated.mjs';
import { readBuffer as readManualEditsBuffer, writeBuffer as writeManualEditsBuffer } from './live-manual-edits-buffer.mjs';

const EXTENSIONS = ['.html', '.jsx', '.tsx', '.vue', '.svelte', '.astro'];

Expand Down Expand Up @@ -92,10 +93,47 @@ Output (JSON):
if (result.carbonize) {
result.todo = 'REQUIRED before next poll: carbonize cleanup in ' + relFile + '. See reference/live.md "Required after accept".';
}
// Scrub stash entries whose originalText was inside the just-replaced
// wrap block. The accept embodies those manual edits (wrap was buffer-
// aware), so the pending ops are now redundant. Bounded to one file read.
if (result.handled !== false) {
try {
scrubManualEditsAgainstFile(targetFile);
} catch {
// Non-fatal; the buffer stays as-is and the user can discard later.
}
}
console.log(JSON.stringify({ handled: true, file: relFile, ...result }));
}
}

/**
* After a variant accept rewrites a portion of targetFile, drop buffer ops
* whose originalText no longer appears in that file. Those ops were almost
* certainly inside the replaced wrap block (which previously contained the
* manual-edited text via buffer-aware wrap), and the accept now embodies them.
*
* Ops whose originalText still appears in targetFile are left alone — they
* relate to other elements the user manually edited but didn't put through the
* variants pipeline.
*/
function scrubManualEditsAgainstFile(targetFile, cwd = process.cwd()) {
const buffer = readManualEditsBuffer(cwd);
if (buffer.entries.length === 0) return;
const fileContent = fs.readFileSync(targetFile, 'utf-8');
let mutated = false;
for (const entry of buffer.entries) {
const before = entry.ops.length;
entry.ops = entry.ops.filter((op) => {
if (!op.originalText) return true;
return fileContent.includes(op.originalText);
});
if (entry.ops.length !== before) mutated = true;
}
buffer.entries = buffer.entries.filter((entry) => entry.ops.length > 0);
if (mutated) writeManualEditsBuffer(cwd, buffer);
}

// ---------------------------------------------------------------------------
// Discard
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -592,4 +630,4 @@ if (_running?.endsWith('live-accept.mjs') || _running?.endsWith('live-accept.mjs
acceptCli();
}

export { findMarkerBlock, extractOriginal, extractVariant, extractCss, deindentContent, detectCommentSyntax };
export { findMarkerBlock, extractOriginal, extractVariant, extractCss, deindentContent, detectCommentSyntax, scrubManualEditsAgainstFile };
Loading
Loading