OR Key
drop another .md file to compare - side-by-side diff against chat-edit

chat-edit

Edits a message already in your chat without starting over.
personal 2 files 3 recent evals

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.

Work with me
For developers how this skill is built, graded, and how it runs

at a glance- the short version

eval modeauto-shape
categoryChat

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.

The skill
state/skills/chat-edit/SKILL.md present
the skill itself, in plain text
The main file. It says what the skill is and lays out the steps in plain English.
Code
state/lib/chat-edit.ts present
code the skill can run
Reusable code this skill can call when it needs to.
Scripts
state/bin/chat-edit/ not present
helper scripts
Optional. Added when a skill has a few commands to run.
Loader
state/skills/chat-edit/AGENTS.md present
what the AI loads on the fly
Loaded automatically the moment this skill is needed. Kept short on purpose.

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.

makes the work The worker
not present

No work step here. This is probably a skill that reads or coordinates, not one that produces something.

checks the work The reviewer
inferred
shape gate an automatic check
The check is an automatic pass or fail on the shape of the result, run separately from the work itself.
frame
learns Self-correction
present
fixes itself learns from gaps
When a run hits a gap, the skill gets edited on the spot [FIXED] or queued for a bigger rewrite [LOGGED], so it keeps getting better.
tidies up Background fixes
present
queued for rewrite runs in the background
Bigger fixes that can't be made on the spot get queued and rewritten in the background later.
remembers Run history
present
state/log/evals.ndjson auto-shape runs
Every run is written down here, so the next time this skill is used it already knows how the last runs went.
Critical rules the things this skill must not get wrong
No must-not-break rules called out for this skill. Anything important lives in the writeup below.

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.

  1. 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-drive to 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

  1. Obtain the messageId of the message to edit (from the thread store or from the TEXT_MESSAGE_START event that created it).
  2. POST to /chat-edit on the head-screen server:
   curl -XPOST 127.0.0.1:3147/chat-edit \
     -H "Content-Type: application/json" \
     -d '{"messageId":"<id>","content":"<new text>"}'
  1. The server pushes a TEXT_MESSAGE_EDIT SSE event. The React reducer in App.tsx applies it to the existing message in the store.
  2. 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-edit endpoint (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

  1. Mutations only, no new messages. This skill patches an existing messageId. For new intent, use chat-drive.
  2. messageId is required. The caller must know the message's ID before calling. No auto-discovery from the rendered DOM.
  3. Head-screen server must be alive. Pre-flight: curl -s 127.0.0.1:3147/healthz must answer.
  4. Endpoint may not be wired yet. If /chat-edit returns 404, the endpoint hasn't been added to server.ts. File the gap and route to a full re-dispatch via chat-drive instead.
  5. Audit by screenshot. The edit event is fire-and-forget from the server side. Use state/lib/desktop.ts capture-screen to verify the render updated.

Commands

| ui dashboard | state/skills/chat-edit/resources/ui.openui |

operationcommand
edit messagecurl -XPOST 127.0.0.1:3147/chat-edit -H "Content-Type: application/json" -d '{"messageId":"<id>","content":"<text>"}'
preflightcurl -s 127.0.0.1:3147/healthz
screenshotnpx tsx state/lib/desktop.ts capture-screen /tmp/chat-edit-verify.png
serverbash state/bin/head-screen/launch.sh (idempotent)

Self-Test

  • [ ] Have the messageId before 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: branded in 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

rubric auto-shape no rubric declared
recent mean 1.00 · 3 runs actor/auditor: unverifiable
deps none declared
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 - -