Skip to content

System Prompt Builder — C++ source

Assemble a system prompt from ordered blocks — role, context, constraints, output format — with a live token count, soft-limit warnings, and a shareable URL. 100% client-side.

This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// system-prompt-builder — C++ port: assemble an ordered list of prompt blocks
// into a markdown-structured system prompt, with pure list operations,
// presets, warnings, and a compact URL codec for shareable state.
//
// Language: C++ (C++17, standard library only)
// Source:   CosmoDev polyglot showcase port of the System Prompt Builder
//           tool, ported from src/lib/systemPromptBuilder.ts (the canonical
//           TypeScript implementation).
// Tool page: https://dev.cosmolabs.org/tools/system-prompt-builder
// License:  display source — part of CosmoDev's polyglot tool pages.
//
// Token counting inlines the chars-per-token heuristic from
// src/lib/tokenEstimator.ts (the original imports it); line lengths are
// counted in characters (the original counts UTF-16 code units, which only
// differs for astral-plane text). The URL codec hand-rolls base64url and a
// minimal JSON codec because the standard library ships neither.

#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

// One editable section of the system prompt.
struct PromptBlock {
    std::string id;
    std::string title;
    std::string content;
    bool enabled = true;
};

// A starter template from the recommended prompt skeleton.
struct PromptPreset {
    const char* id;
    const char* title;
    const char* description;
    const char* content;
};

// Assemble + count + lint in one pass — the island's live report.
struct PromptReport {
    std::string assembled;
    long long tokens = 0;
    std::vector<std::string> warnings;
};

// Selects the chars-per-token rate for buildReport ('auto' classifies each
// line by its shape, as src/lib/tokenEstimator.ts does).
enum class ContentType { Prose, Code, Json, Cjk, Auto };

// Fields to patch on one block; nullopt leaves that field untouched.
struct PromptBlockPatch {
    std::optional<std::string> title;
    std::optional<std::string> content;
    std::optional<bool> enabled;
};

// Blocks whose assembled size starts crowding the context on most models.
constexpr int kSystemPromptSoftLimitTokens = 2000;

// True when the encoded form would make an uncomfortably long URL.
constexpr int kMaxEncodedLength = 4000;

// Ordered starter templates — the recommended skeleton of a system prompt.
const std::vector<PromptPreset>& systemPromptPresets() {
    static const std::vector<PromptPreset> presets = {
        {"role", "Role", "Who the model is and what it optimizes for.",
         "You are a senior software engineer. You give correct, concise answers and say so "
         "plainly when you are unsure."},
        {"context", "Context", "The situation the model is working in.",
         "The user is a developer working in a TypeScript codebase. Prefer runnable examples "
         "over prose when both work."},
        {"constraints", "Constraints", "Hard rules the model must not break.",
         "- Never invent library APIs; use only the ones in the provided code.\n"
         "- Keep answers under 300 words unless asked for more."},
        {"output-format", "Output format", "The exact shape of the answer.",
         "Respond with: 1) a one-line summary, 2) a fenced code block, 3) any caveats as "
         "bullet points."},
        {"examples", "Examples", "Few-shot demonstrations of the desired behavior.",
         "Input: reverse \"abc\"\nOutput: \"cba\""},
        {"tone", "Tone", "Voice and register.", "Direct and friendly. No filler openers, no apologies."},
        {"refusal", "Refusal policy", "How to handle out-of-scope requests.",
         "If a request is outside your scope, say so in one sentence and suggest the closest "
         "thing you can do."},
        {"safety", "Safety", "Guardrails for sensitive content.",
         "Refuse requests that could cause harm, and never echo secrets, keys, or credentials "
         "back in full."},
    };
    return presets;
}

// ---- token estimate (tokens figure only, from tokenEstimator.ts) ------------

namespace {

enum class LineType { Prose, Code, Json, Cjk };

double charsPerToken(LineType t) {
    switch (t) {
        case LineType::Prose: return 4.0;
        case LineType::Code: return 3.5;
        case LineType::Json: return 3.0;
        case LineType::Cjk: return 1.5;
    }
    return 4.0;
}

// CJK ideographs / kana / Hangul pack roughly one token per 1.5 chars.
bool isCjk(char32_t cp) {
    return (cp >= 0x4E00 && cp <= 0x9FFF) || (cp >= 0x3040 && cp <= 0x30FF) ||
           (cp >= 0xAC00 && cp <= 0xD82F);
}

bool hasCjk(const std::string& s) {
    size_t i = 0;
    const size_t n = s.size();
    while (i < n) {
        const unsigned char b = static_cast<unsigned char>(s[i]);
        char32_t cp;
        if (b < 0x80) {
            cp = b;
            i += 1;
        } else if ((b & 0xE0) == 0xC0) {
            if (i + 1 >= n) return false;
            cp = (static_cast<char32_t>(b & 0x1F) << 6) | (s[i + 1] & 0x3F);
            i += 2;
        } else if ((b & 0xF0) == 0xE0) {
            if (i + 2 >= n) return false;
            cp = (static_cast<char32_t>(b & 0x0F) << 12) | (static_cast<char32_t>(s[i + 1] & 0x3F) << 6) |
                 (s[i + 2] & 0x3F);
            i += 3;
        } else if ((b & 0xF8) == 0xF0) {
            if (i + 3 >= n) return false;
            cp = (static_cast<char32_t>(b & 0x07) << 18) | (static_cast<char32_t>(s[i + 1] & 0x3F) << 12) |
                 (static_cast<char32_t>(s[i + 2] & 0x3F) << 6) | (s[i + 3] & 0x3F);
            i += 4;
        } else {
            return false;  // invalid UTF-8 lead byte
        }
        if (isCjk(cp)) return true;
    }
    return false;
}

}  // namespace

// ---- minimal JSON value tree + parser (shared by the codec and probe) -------

namespace jsonmini {

struct Value {
    enum class Kind { Null, Bool, Num, Str, Arr, Obj };
    Kind kind = Kind::Null;
    bool boolean = false;
    double num = 0;
    std::string str;
    std::vector<Value> arr;
    std::vector<std::pair<std::string, Value>> obj;
};

class Parser {
public:
    explicit Parser(const std::string& text) : s_(text), i_(0) {}

    // Parses one value and requires the rest to be whitespace; nullopt on any
    // RFC 8259 violation.
    static std::optional<Value> parse(const std::string& text) {
        Parser p(text);
        std::optional<Value> v = p.parseValue();
        if (!v) return std::nullopt;
        p.skipWs();
        if (p.i_ != p.s_.size()) return std::nullopt;
        return v;
    }

private:
    void skipWs() {
        while (i_ < s_.size() && (s_[i_] == ' ' || s_[i_] == '\t' || s_[i_] == '\n' || s_[i_] == '\r')) i_++;
    }
    bool eof() const { return i_ >= s_.size(); }
    char peek() const { return s_[i_]; }

    bool expectLit(const char* lit) {
        size_t n = std::char_traits<char>::length(lit);
        if (s_.compare(i_, n, lit) != 0) return false;
        i_ += n;
        return true;
    }

    std::optional<Value> parseValue() {
        skipWs();
        if (eof()) return std::nullopt;
        switch (peek()) {
            case 'n': return expectLit("null") ? std::optional<Value>(Value{}) : std::nullopt;
            case 't': {
                if (!expectLit("true")) return std::nullopt;
                Value v;
                v.kind = Value::Kind::Bool;
                v.boolean = true;
                return v;
            }
            case 'f': {
                if (!expectLit("false")) return std::nullopt;
                Value v;
                v.kind = Value::Kind::Bool;
                v.boolean = false;
                return v;
            }
            case '"': {
                std::optional<std::string> str = parseString();
                if (!str) return std::nullopt;
                Value v;
                v.kind = Value::Kind::Str;
                v.str = std::move(*str);
                return v;
            }
            case '[': return parseArray();
            case '{': return parseObject();
            default: return parseNumber();
        }
    }

    std::optional<std::string> parseString() {
        i_++;  // opening quote
        std::string out;
        while (true) {
            if (eof()) return std::nullopt;
            char c = s_[i_++];
            if (c == '"') return out;
            if (c != '\\') {
                if (static_cast<unsigned char>(c) < 0x20) return std::nullopt;
                out.push_back(c);  // multi-byte UTF-8 passes through
                continue;
            }
            if (eof()) return std::nullopt;
            char esc = s_[i_++];
            switch (esc) {
                case '"': out.push_back('"'); break;
                case '\\': out.push_back('\\'); break;
                case '/': out.push_back('/'); break;
                case 'b': out.push_back('\b'); break;
                case 'f': out.push_back('\f'); break;
                case 'n': out.push_back('\n'); break;
                case 'r': out.push_back('\r'); break;
                case 't': out.push_back('\t'); break;
                case 'u': {
                    std::optional<char32_t> hi = parseHex4();
                    if (!hi) return std::nullopt;
                    // Combine a surrogate pair when present; a lone surrogate
                    // becomes U+FFFD (what a browser's TextDecoder emits).
                    char32_t cp = *hi;
                    if (cp >= 0xD800 && cp <= 0xDBFF) {
                        if (i_ + 1 < s_.size() && s_[i_] == '\\' && s_[i_ + 1] == 'u') {
                            i_ += 2;
                            std::optional<char32_t> lo = parseHex4();
                            if (!lo) return std::nullopt;
                            cp = (*lo >= 0xDC00 && *lo <= 0xDFFF)
                                     ? 0x10000 + ((cp - 0xD800) << 10) + (*lo - 0xDC00)
                                     : 0xFFFD;
                        } else {
                            cp = 0xFFFD;
                        }
                    } else if (cp >= 0xDC00 && cp <= 0xDFFF) {
                        cp = 0xFFFD;
                    }
                    appendUtf8(out, cp);
                    break;
                }
                default: return std::nullopt;
            }
        }
    }

    std::optional<char32_t> parseHex4() {
        if (i_ + 4 > s_.size()) return std::nullopt;
        char32_t v = 0;
        for (int k = 0; k < 4; k++) {
            char h = s_[i_ + static_cast<size_t>(k)];
            v <<= 4;
            if (h >= '0' && h <= '9') v |= static_cast<char32_t>(h - '0');
            else if (h >= 'a' && h <= 'f') v |= static_cast<char32_t>(h - 'a' + 10);
            else if (h >= 'A' && h <= 'F') v |= static_cast<char32_t>(h - 'A' + 10);
            else return std::nullopt;
        }
        i_ += 4;
        return v;
    }

    static void appendUtf8(std::string& out, char32_t cp) {
        if (cp < 0x80) {
            out.push_back(static_cast<char>(cp));
        } else if (cp < 0x800) {
            out.push_back(static_cast<char>(0xC0 | (cp >> 6)));
            out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
        } else if (cp < 0x10000) {
            out.push_back(static_cast<char>(0xE0 | (cp >> 12)));
            out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
            out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
        } else {
            out.push_back(static_cast<char>(0xF0 | (cp >> 18)));
            out.push_back(static_cast<char>(0x80 | ((cp >> 12) & 0x3F)));
            out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
            out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
        }
    }

    std::optional<Value> parseNumber() {
        const size_t start = i_;
        if (!eof() && peek() == '-') i_++;
        while (!eof() && peek() >= '0' && peek() <= '9') i_++;
        if (!eof() && peek() == '.') {
            i_++;
            while (!eof() && peek() >= '0' && peek() <= '9') i_++;
        }
        if (!eof() && (peek() == 'e' || peek() == 'E')) {
            i_++;
            if (!eof() && (peek() == '+' || peek() == '-')) i_++;
            while (!eof() && peek() >= '0' && peek() <= '9') i_++;
        }
        if (i_ == start) return std::nullopt;
        Value v;
        v.kind = Value::Kind::Num;
        v.num = std::stod(s_.substr(start, i_ - start));
        return v;
    }

    std::optional<Value> parseArray() {
        i_++;  // '['
        Value v;
        v.kind = Value::Kind::Arr;
        skipWs();
        if (!eof() && peek() == ']') {
            i_++;
            return v;
        }
        while (true) {
            std::optional<Value> item = parseValue();
            if (!item) return std::nullopt;
            v.arr.push_back(std::move(*item));
            skipWs();
            if (eof()) return std::nullopt;
            if (peek() == ',') { i_++; continue; }
            if (peek() == ']') { i_++; return v; }
            return std::nullopt;
        }
    }

    std::optional<Value> parseObject() {
        i_++;  // '{'
        Value v;
        v.kind = Value::Kind::Obj;
        skipWs();
        if (!eof() && peek() == '}') {
            i_++;
            return v;
        }
        while (true) {
            skipWs();
            std::optional<std::string> key = parseString();
            if (!key) return std::nullopt;
            skipWs();
            if (eof() || peek() != ':') return std::nullopt;
            i_++;
            std::optional<Value> value = parseValue();
            if (!value) return std::nullopt;
            v.obj.emplace_back(std::move(*key), std::move(*value));
            skipWs();
            if (eof()) return std::nullopt;
            if (peek() == ',') { i_++; continue; }
            if (peek() == '}') { i_++; return v; }
            return std::nullopt;
        }
    }

    const std::string& s_;
    size_t i_;
};

}  // namespace jsonmini

namespace {

// Classify a single line by its shape. Order: json, cjk, code, prose.
LineType detectLineType(const std::string& line) {
    const std::string trimmed = [](std::string t) {
        size_t a = t.find_first_not_of(" \t\n\r\v\f");
        size_t b = t.find_last_not_of(" \t\n\r\v\f");
        return a == std::string::npos ? std::string() : t.substr(a, b - a + 1);
    }(line);
    // JSON-ish: opens like a JSON fragment AND carries a separator.
    const char first = trimmed.empty() ? '\0' : trimmed.front();
    if ((first == '{' || first == '}' || first == '[' || first == '"') &&
        (line.find(':') != std::string::npos || line.find(',') != std::string::npos)) {
        return LineType::Json;
    }
    if (hasCjk(line)) return LineType::Cjk;
    // Code: symbol-dense, or a statement terminator / block opener at EOL.
    const size_t chars = line.size();
    size_t symbols = 0;
    for (char c : line) {
        if (std::string_view("{}();=<>[]#").find(c) != std::string_view::npos) symbols++;
    }
    const double density = chars == 0 ? 0.0 : static_cast<double>(symbols) / static_cast<double>(chars);
    if (density > 0.08 || (!trimmed.empty() && (trimmed.back() == ';' || trimmed.back() == '{' ||
                                                trimmed.back() == '}'))) {
        return LineType::Code;
    }
    return LineType::Prose;
}

// Sum of per-line token estimates (excludes chat framing).
long long estimateTokens(const std::string& text, ContentType contentType) {
    LineType forced = LineType::Prose;
    bool hasForced = true;
    switch (contentType) {
        case ContentType::Prose: forced = LineType::Prose; break;
        case ContentType::Code: forced = LineType::Code; break;
        case ContentType::Json: forced = LineType::Json; break;
        case ContentType::Cjk: forced = LineType::Cjk; break;
        case ContentType::Auto: hasForced = false; break;
    }
    // AUTO + whole-text JSON: a document that parses as JSON is json all the
    // way down.
    bool wholeTextJson = false;
    {
        std::string trimmed = text;
        trimmed.erase(0, trimmed.find_first_not_of(" \t\n\r\v\f"));
        if (!hasForced && !trimmed.empty()) {
            wholeTextJson = jsonmini::Parser::parse(text).has_value();
        }
    }
    long long tokens = 0;
    size_t start = 0;
    while (start <= text.size()) {
        size_t nl = text.find('\n', start);
        size_t end = nl == std::string::npos ? text.size() : nl;
        std::string line = text.substr(start, end - start);
        if (!line.empty() && line.back() == '\r') line.pop_back();  // split on \r?\n
        bool blank = line.find_first_not_of(" \t\n\r\v\f") == std::string::npos;
        if (!blank) {
            LineType t = hasForced ? forced
                                   : (wholeTextJson ? LineType::Json : detectLineType(line));
            // max(1, round(len / rate)) — floor(x + 0.5) is JS-style rounding.
            double est = std::floor(static_cast<double>(line.size()) / charsPerToken(t) + 0.5);
            tokens += static_cast<long long>(est < 1.0 ? 1.0 : est);
        }
        if (nl == std::string::npos) break;
        start = nl + 1;
    }
    return tokens;
}

// Group integer digits with commas the way toLocaleString('en-US') does.
std::string thousands(long long n) {
    std::string digits = std::to_string(n);
    std::string out;
    const size_t len = digits.size();
    for (size_t i = 0; i < len; i++) {
        if (i > 0 && (len - i) % 3 == 0 && digits[i - 1] != '-') out.push_back(',');
        out.push_back(digits[i]);
    }
    return out;
}

constexpr const char* kBase64UrlAlphabet =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";

// Standard base64 with the URL-safe alphabet and no trailing padding.
std::string toBase64Url(const std::string& data) {
    std::string out;
    out.reserve(data.size() / 3 * 4 + 4);
    for (size_t i = 0; i < data.size(); i += 3) {
        const unsigned b0 = static_cast<unsigned char>(data[i]);
        const unsigned b1 = i + 1 < data.size() ? static_cast<unsigned char>(data[i + 1]) : 0u;
        const unsigned b2 = i + 2 < data.size() ? static_cast<unsigned char>(data[i + 2]) : 0u;
        const unsigned n = (b0 << 16) | (b1 << 8) | b2;
        out.push_back(kBase64UrlAlphabet[(n >> 18) & 0x3F]);
        out.push_back(kBase64UrlAlphabet[(n >> 12) & 0x3F]);
        if (i + 1 < data.size()) out.push_back(kBase64UrlAlphabet[(n >> 6) & 0x3F]);
        if (i + 2 < data.size()) out.push_back(kBase64UrlAlphabet[n & 0x3F]);
    }
    return out;
}

int base64UrlVal(char c) {
    if (c >= 'A' && c <= 'Z') return c - 'A';
    if (c >= 'a' && c <= 'z') return c - 'a' + 26;
    if (c >= '0' && c <= '9') return c - '0' + 52;
    if (c == '-') return 62;
    if (c == '_') return 63;
    return -1;
}

// Inverse of toBase64Url; nullopt on invalid characters or length.
std::optional<std::string> fromBase64Url(const std::string& s) {
    if (s.size() % 4 == 1) return std::nullopt;
    std::string out;
    out.reserve(s.size() / 4 * 3 + 3);
    for (size_t i = 0; i < s.size(); i += 4) {
        const size_t m = std::min<size_t>(4, s.size() - i);
        int v[4] = {0, 0, 0, 0};
        for (size_t k = 0; k < m; k++) {
            v[k] = base64UrlVal(s[i + k]);
            if (v[k] < 0) return std::nullopt;
        }
        out.push_back(static_cast<char>((v[0] << 2) | (v[1] >> 4)));
        if (m > 2) out.push_back(static_cast<char>(((v[1] & 0xF) << 4) | (v[2] >> 2)));
        if (m > 3) out.push_back(static_cast<char>(((v[2] & 0x3) << 2) | v[3]));
    }
    return out;
}

// Append s as a quoted JSON string, escaping exactly like JSON.stringify.
void pushJsonString(std::string& out, const std::string& s) {
    out.push_back('"');
    for (char c : s) {
        switch (c) {
            case '"': out += "\\\""; break;
            case '\\': out += "\\\\"; break;
            case '\b': out += "\\b"; break;
            case '\f': out += "\\f"; break;
            case '\n': out += "\\n"; break;
            case '\r': out += "\\r"; break;
            case '\t': out += "\\t"; break;
            default:
                if (static_cast<unsigned char>(c) < 0x20) {
                    char hex[8];
                    std::snprintf(hex, sizeof hex, "\\u%04x", c);
                    out += hex;
                } else {
                    out.push_back(c);
                }
        }
    }
    out.push_back('"');
}

std::string trimCopy(const std::string& s) {
    size_t a = s.find_first_not_of(" \t\n\r\v\f");
    if (a == std::string::npos) return std::string();
    size_t b = s.find_last_not_of(" \t\n\r\v\f");
    return s.substr(a, b - a + 1);
}

}  // namespace

// ---- public API ---------------------------------------------------------------

// Render enabled, non-empty blocks (in order) as one markdown-structured
// prompt (drop the "## Title" headers by passing headers = false).
std::string assemblePrompt(const std::vector<PromptBlock>& blocks, bool headers = true) {
    std::vector<std::string> rendered;
    for (const PromptBlock& b : blocks) {
        std::string content = trimCopy(b.content);
        if (!b.enabled || content.empty()) continue;
        if (headers) {
            std::string title = trimCopy(b.title);
            rendered.push_back("## " + (title.empty() ? std::string("Untitled") : title) + "\n" + content);
        } else {
            rendered.push_back(std::move(content));
        }
    }
    std::string joined;
    for (size_t i = 0; i < rendered.size(); i++) {
        if (i > 0) joined += "\n\n";
        joined += rendered[i];
    }
    return trimCopy(joined);
}

// Append a block (caller supplies the id so the lib stays pure). The original
// defaults content to "" and enabled to true.
std::vector<PromptBlock> addBlock(std::vector<PromptBlock> blocks, const std::string& id,
                                  const std::string& title, const std::string& content = "",
                                  bool enabled = true) {
    blocks.push_back(PromptBlock{id, title, content, enabled});
    return blocks;
}

// Patch one block by id; unknown ids leave the list unchanged.
std::vector<PromptBlock> updateBlock(std::vector<PromptBlock> blocks, const std::string& id,
                                     const PromptBlockPatch& patch) {
    for (PromptBlock& b : blocks) {
        if (b.id != id) continue;
        if (patch.title) b.title = *patch.title;
        if (patch.content) b.content = *patch.content;
        if (patch.enabled) b.enabled = *patch.enabled;
    }
    return blocks;
}

// Flip one block's enabled flag by id.
std::vector<PromptBlock> toggleBlock(std::vector<PromptBlock> blocks, const std::string& id) {
    for (PromptBlock& b : blocks) {
        if (b.id == id) b.enabled = !b.enabled;
    }
    return blocks;
}

// Remove one block by id.
std::vector<PromptBlock> removeBlock(const std::vector<PromptBlock>& blocks, const std::string& id) {
    std::vector<PromptBlock> out;
    out.reserve(blocks.size());
    for (const PromptBlock& b : blocks) {
        if (b.id != id) out.push_back(b);
    }
    return out;
}

// Move a block (no-op when the indexes are out of range or equal — the
// original clamps negatives the same way because they are out of range).
std::vector<PromptBlock> moveBlock(const std::vector<PromptBlock>& blocks, size_t from, size_t to) {
    if (from >= blocks.size() || to >= blocks.size() || from == to) return blocks;
    std::vector<PromptBlock> next = blocks;
    PromptBlock moved = next[from];
    next.erase(next.begin() + static_cast<long>(from));
    next.insert(next.begin() + static_cast<long>(to), std::move(moved));
    return next;
}

// Assemble + count + lint in one pass — the island's live report.
PromptReport buildReport(const std::vector<PromptBlock>& blocks,
                         ContentType contentType = ContentType::Prose) {
    PromptReport report;
    report.assembled = assemblePrompt(blocks, true);
    report.tokens = report.assembled.empty() ? 0 : estimateTokens(report.assembled, contentType);
    if (report.tokens > kSystemPromptSoftLimitTokens) {
        report.warnings.push_back(
            "Assembled prompt is ~" + thousands(report.tokens) + " tokens — beyond " +
            thousands(kSystemPromptSoftLimitTokens) +
            " it starts crowding the context window on most models.");
    }
    bool hasRole = false;
    for (const PromptBlock& b : blocks) {
        std::string title = trimCopy(b.title);
        for (char& c : title) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
        if (b.enabled && title == "role") {
            hasRole = true;
            break;
        }
    }
    if (!blocks.empty() && !hasRole) {
        report.warnings.push_back(
            "No enabled \"Role\" block — stating who the model is tends to anchor every "
            "following instruction.");
    }
    if (!blocks.empty() && report.assembled.empty()) {
        report.warnings.push_back("Every block is disabled or empty — the assembled prompt is empty.");
    }
    return report;
}

// ---- shareable state codec (URL-safe, compact) -------------------------------
// Triples of [enabled(0/1), title, content] keep URLs far smaller than the
// full object shape; ids are regenerated on decode (they are UI-local).

// Encode blocks to a compact base64url string; "" when blocks are empty.
std::string encodeBlocks(const std::vector<PromptBlock>& blocks) {
    if (blocks.empty()) return std::string();
    std::string json = "[";
    for (size_t i = 0; i < blocks.size(); i++) {
        if (i > 0) json.push_back(',');
        json.push_back('[');
        json.push_back(blocks[i].enabled ? '1' : '0');
        json.push_back(',');
        pushJsonString(json, blocks[i].title);
        json.push_back(',');
        pushJsonString(json, blocks[i].content);
        json.push_back(']');
    }
    json.push_back(']');
    return toBase64Url(json);
}

// True when the encoded form would make an uncomfortably long URL.
bool encodedTooLong(const std::string& encoded) {
    return encoded.size() > static_cast<size_t>(kMaxEncodedLength);
}

// Decode encodeBlocks output; regenerates ids (b1, b2, …). Returns nullopt
// on malformed input — never throws; "" decodes to an empty list.
std::optional<std::vector<PromptBlock>> decodeBlocks(const std::string& encoded) {
    if (encoded.empty()) return std::vector<PromptBlock>{};
    std::optional<std::string> bytes = fromBase64Url(encoded);
    if (!bytes) return std::nullopt;
    std::optional<jsonmini::Value> raw = jsonmini::Parser::parse(*bytes);
    if (!raw || raw->kind != jsonmini::Value::Kind::Arr) return std::nullopt;
    std::vector<PromptBlock> blocks;
    blocks.reserve(raw->arr.size());
    for (size_t i = 0; i < raw->arr.size(); i++) {
        const jsonmini::Value& entry = raw->arr[i];
        if (entry.kind != jsonmini::Value::Kind::Arr || entry.arr.size() != 3) return std::nullopt;
        const jsonmini::Value& enabled = entry.arr[0];
        const jsonmini::Value& title = entry.arr[1];
        const jsonmini::Value& content = entry.arr[2];
        if (enabled.kind != jsonmini::Value::Kind::Num || title.kind != jsonmini::Value::Kind::Str ||
            content.kind != jsonmini::Value::Kind::Str) {
            return std::nullopt;
        }
        blocks.push_back(PromptBlock{"b" + std::to_string(i + 1), title.str, content.str,
                                     enabled.num == 1.0});
    }
    return blocks;
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →