No work step here. This is probably a skill that reads or coordinates, not one that produces something.
.md file to compare - side-by-side diff against chat-edit
chat-edit
What it does for you
Edits a message already in your chat without starting over.
What it produces
A recent result, so you can see the kind of work it returns.
loading…
How to get it
These run inside the Snappy workspace. Want this working in your business? I set skills like this up with you, in one focused week.
For developers how this skill is built, graded, and how it runs
at a glance- the short version
what's inside - the parts that make up a skill 3/4 present
A skill is just a few plain-text files. Only the main one is required. The rest are optional, added as the work needs them. This is what the skill is made of; how it runs is just below.
state/skills/chat-edit/SKILL.md
present
state/lib/chat-edit.ts
present
state/bin/chat-edit/
not present
state/skills/chat-edit/AGENTS.md
present
how it runs - the shared frame every skill uses 3/5 present
Every skill runs the same way. One part does the work, a separate part checks it, and a short loader hands the AI exactly what it needs for the job. Anything this skill doesn't use shows a one-line note saying why, on purpose, not by accident.
state/log/evals.ndjson what it has learned - fixes written back in over time sample
When a run hits something this skill didn't handle, the fix gets written back into the skill so it doesn't happen again. FIXED means it was corrected on the spot. LOGGED means it's queued for a bigger rewrite. Either way, the skill gets a little better and never makes the same mistake twice.
- Loading feedback rows…
SKILL.md- the skill, written out in plain English
chat-edit
Modify an existing message in the snappy-chat thread. The edit flows directly into the OpenUI store, updating the rendered card in-place without triggering a new dispatch.
What it's for
- Correction flows. An agent spots an error in a rendered card and patches it without re-running the full dispatch.
- Streaming patch. Append or replace content in an assistant message mid-stream, useful when a long dispatch produces incremental refinements.
- Inline annotation. Tag or annotate a rendered card with a score, status, or metadata field after the fact.
When NOT to use it
- New messages. Use
chat-driveto push new intent. This skill only mutates existing messages. - Structural reflows. If the card's component type needs to change, re-dispatch via
chat-drive- the store doesn't support type-swapping on edit.
Steps
- Obtain the
messageIdof the message to edit (from the thread store or from theTEXT_MESSAGE_STARTevent that created it). - POST to
/chat-editon the head-screen server:
curl -XPOST 127.0.0.1:3147/chat-edit \
-H "Content-Type: application/json" \
-d '{"messageId":"<id>","content":"<new text>"}'
- The server pushes a
TEXT_MESSAGE_EDITSSE event. The React reducer inApp.tsxapplies it to the existing message in the store. - Audit by screenshot:
npx tsx state/lib/desktop.ts capture-screen /tmp/chat-edit-verify.png.
Eval
Kind: auto-shape. Frontmatter + AGENTS.md presence passes the gate. Behavioral test pending the /chat-edit endpoint being wired in state/bin/head-screen/server.ts.
Files
state/bin/head-screen/server.ts- owns the/chat-editendpoint (pending wire-up).state/skills/chat-edit/SKILL.md- this file.
AGENTS.md- what the AI loads when this skill comes up
chat-edit - loader
Per-turn rules. Full reference: state/skills/chat-edit/SKILL.md.
Critical Rules
- Mutations only, no new messages. This skill patches an existing
messageId. For new intent, usechat-drive. messageIdis required. The caller must know the message's ID before calling. No auto-discovery from the rendered DOM.- Head-screen server must be alive. Pre-flight:
curl -s 127.0.0.1:3147/healthzmust answer. - Endpoint may not be wired yet. If
/chat-editreturns 404, the endpoint hasn't been added toserver.ts. File the gap and route to a full re-dispatch viachat-driveinstead. - Audit by screenshot. The edit event is fire-and-forget from the server side. Use
state/lib/desktop.tscapture-screen to verify the render updated.
Commands
| ui dashboard | state/skills/chat-edit/resources/ui.openui |
| operation | command |
|---|---|
| edit message | curl -XPOST 127.0.0.1:3147/chat-edit -H "Content-Type: application/json" -d '{"messageId":"<id>","content":"<text>"}' |
| preflight | curl -s 127.0.0.1:3147/healthz |
| screenshot | npx tsx state/lib/desktop.ts capture-screen /tmp/chat-edit-verify.png |
| server | bash state/bin/head-screen/launch.sh (idempotent) |
Self-Test
- [ ] Have the
messageIdbefore calling. - [ ] Verify head-screen is alive before posting.
- [ ] Audit by screenshot, not return code.
Self-correcting loader (PID feedback)
echo "[$(date -u +%FT%TZ)] chat-edit: <what was missing or fixed> [FIXED|LOGGED] action_kind=skill-ran" >> state/log/loader-feedback.log
OpenUI Resource
- Skill-owned OpenUI Lang resource:
state/skills/chat-edit/resources/ui.openui. Read it before rendering or editing this skill's generated component surface. - Treat this resource as a first-class artifact of the skill, not a generic chat response. Improve it when the skill's user-facing output needs to become richer.
- System resources compose OpenUI primitives and inherit SnappyChat tokens. Use
ui_contract: brandedin SKILL.md only for deliberate platform or client visuals.
api.ts- the code it can call
#!/usr/bin/env npx tsx
/**
* snappy-chat-edit/api.ts
*
* Edit surface for snappy-chat: preview a unified diff before applying.
* Supports file reads, validation, preview generation, and atomic writes.
* Used by server.ts /edit/preview and /edit/apply endpoints.
*
* Exports:
* - previewEdit(path, oldText, newText) → {token, diff, ok, error}
* - applyEdit(path, oldText, newText) → {ok, error}
*
* Token expiry: 5 minutes (in-memory map in server.ts, not persisted).
*/
import { readFileSync, writeFileSync, existsSync, renameSync, unlinkSync } from "fs";
import { dirname } from "path";
/**
* Unified diff generator. Simple line-by-line diff for readability.
* Output format: each line prefixed with " " (context), "-" (removed), or "+" (added).
* Returned as a single string with embedded \n characters (not literal newlines).
*/
export function generateUnifiedDiff(oldText: string, newText: string): string {
const oldLines = oldText.split("\n");
const newLines = newText.split("\n");
const diff: string[] = [];
// Simple line-level diff (not a full Myers algorithm, but sufficient for UI).
let oldIdx = 0;
let newIdx = 0;
while (oldIdx < oldLines.length || newIdx < newLines.length) {
if (oldIdx < oldLines.length && newIdx < newLines.length) {
if (oldLines[oldIdx] === newLines[newIdx]) {
diff.push(` ${oldLines[oldIdx]}`);
oldIdx++;
newIdx++;
} else {
// Mismatch: collect all consecutive old lines, then all consecutive new lines
const oldStart = oldIdx;
while (oldIdx < oldLines.length && newIdx < newLines.length && oldLines[oldIdx] !== newLines[newIdx]) {
oldIdx++;
}
// Check if we've reached the end or found a match
if (oldIdx === oldLines.length || newIdx === newLines.length || oldLines[oldIdx] !== newLines[newIdx]) {
// Still mismatched or end of one; emit what we've collected
for (let i = oldStart; i < oldIdx; i++) {
diff.push(`-${oldLines[i]}`);
}
const newStart = newIdx;
while (newIdx < newLines.length && oldIdx < oldLines.length && oldLines[oldIdx] !== newLines[newIdx]) {
newIdx++;
}
for (let i = newStart; i < newIdx; i++) {
diff.push(`+${newLines[i]}`);
}
} else {
// Found a match; emit old deletions then new additions
for (let i = oldStart; i < oldIdx; i++) {
diff.push(`-${oldLines[i]}`);
}
const newStart = newIdx;
while (newIdx < newLines.length && newLines[newIdx] !== oldLines[oldIdx]) {
newIdx++;
}
for (let i = newStart; i < newIdx; i++) {
diff.push(`+${newLines[i]}`);
}
}
}
} else if (oldIdx < oldLines.length) {
diff.push(`-${oldLines[oldIdx]}`);
oldIdx++;
} else {
diff.push(`+${newLines[newIdx]}`);
newIdx++;
}
}
// Return as a string with escaped newlines for JSON safety
return diff.join("\n");
}
/**
* Preview an edit: read the file, verify oldText exists exactly once,
* generate a unified diff, and return a token for later application.
*
* Returns: { ok, token?, diff?, error? }
* - ok=true: token and diff are present (apply later with /edit/apply?token=X)
* - ok=false: error message (file not found, oldText mismatch, etc.)
*/
export function previewEdit(
path: string,
oldText: string,
newText: string,
): { ok: boolean; token?: string; diff?: string; error?: string } {
try {
if (!existsSync(path)) {
return { ok: false, error: `file not found: ${path}` };
}
const content = readFileSync(path, "utf8");
// Verify oldText appears exactly once
const count = (content.split(oldText).length - 1);
if (count === 0) {
return { ok: false, error: `oldText not found in file: ${path}` };
}
if (count > 1) {
return { ok: false, error: `oldText appears ${count} times in file (expected exactly 1): ${path}` };
}
const diff = generateUnifiedDiff(content, content.replace(oldText, newText));
const token = `edit-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
return { ok: true, token, diff };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { ok: false, error: `preview failed: ${msg}` };
}
}
/**
* Apply an edit: read the file, verify oldText exists exactly once,
* apply the replacement, and write atomically.
*
* Returns: { ok, error? }
* - ok=true: file written successfully
* - ok=false: error message (file not found, oldText mismatch, etc.)
*/
export function applyEdit(
path: string,
oldText: string,
newText: string,
): { ok: boolean; error?: string } {
try {
if (!existsSync(path)) {
return { ok: false, error: `file not found: ${path}` };
}
const content = readFileSync(path, "utf8");
// Verify oldText appears exactly once
const count = (content.split(oldText).length - 1);
if (count === 0) {
return { ok: false, error: `oldText not found in file: ${path}` };
}
if (count > 1) {
return { ok: false, error: `oldText appears ${count} times in file (expected exactly 1): ${path}` };
}
// Apply the replacement
const updated = content.replace(oldText, newText);
// Atomic write: write to temp, then rename
const tempPath = `${path}.tmp-${Date.now()}`;
try {
writeFileSync(tempPath, updated, "utf8");
// Rename atomic on POSIX systems
renameSync(tempPath, path);
} catch (err) {
try {
unlinkSync(tempPath);
} catch {}
throw err;
}
return { ok: true };
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
return { ok: false, error: `apply failed: ${msg}` };
}
}
scripts- helper scripts it can run
prose-only skill - no sidecar under state/bin/ yet. Steps, if any, are described in SKILL.md.
how we check it- the checks, plus the last 3 runs
| timestamp | verb | score | primary_issue | artifact |
|---|---|---|---|---|
| 2026-05-01 09:19Z | - | 1.00 | - | - |
| 2026-05-01 09:19Z | - | 1.00 | - | - |
| 2026-05-01 09:19Z | - | 1.00 | - | - |