softr-vibe-coding 2.15.0 → 2.15.2
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/CHANGELOG.md +10 -0
- package/README.md +3 -1
- package/SKILL.md +3 -3
- package/package.json +1 -1
- package/references/browser-checks.md +89 -10
- package/references/date-picker.md +1 -1
- package/references/softr-mcp.md +40 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to this skill are documented here. Versions follow [Semantic
|
|
|
4
4
|
|
|
5
5
|
Entries from 1.3.1 onward are generated automatically from git commit subjects between version bumps (see `.github/workflows/publish.yml`). Entries before 1.3.1 were backfilled by hand from the existing commit history.
|
|
6
6
|
|
|
7
|
+
## [2.15.2] - 2026-10-08
|
|
8
|
+
- Release 2.15.2
|
|
9
|
+
- browser-checks: run checks in the client's time zone
|
|
10
|
+
- Release 2.15.1
|
|
11
|
+
- softr-mcp: unicode escapes come back decoded on push
|
|
12
|
+
|
|
13
|
+
## [2.15.1] - 2026-10-08
|
|
14
|
+
- Release 2.15.1
|
|
15
|
+
- softr-mcp: unicode escapes come back decoded on push
|
|
16
|
+
|
|
7
17
|
## [2.15.0] - 2026-10-07
|
|
8
18
|
- Release 2.15.0
|
|
9
19
|
- Add the brand date picker reference
|
package/README.md
CHANGED
|
@@ -220,7 +220,9 @@ softr-vibe-coding/
|
|
|
220
220
|
│ │ # shell, eval measurements, the records-trigger
|
|
221
221
|
│ │ # write guard proven before any click, what a
|
|
222
222
|
│ │ # click sent, screenshots to disk (Oct 1 2026);
|
|
223
|
-
│ │ # testing Custom Code header CSS (Oct 5 2026)
|
|
223
|
+
│ │ # testing Custom Code header CSS (Oct 5 2026);
|
|
224
|
+
│ │ # the client's time zone via TZ at launch, and
|
|
225
|
+
│ │ # date-only values shown vs stored (Oct 8 2026)
|
|
224
226
|
│ ├── advanced-integrations.md # Shadow DOM CSS isolation
|
|
225
227
|
│ │ # Leaflet, Mapbox, TinyMCE, Quill, FullCalendar
|
|
226
228
|
│ ├── native-chrome-styling.md # Restyle Softr's native shell (header, footer,
|
package/SKILL.md
CHANGED
|
@@ -102,8 +102,8 @@ You generate complete, production-ready Softr Vibe Coding blocks as TypeScript R
|
|
|
102
102
|
- Static block: no hardcoded user-visible copy — every string/image/link is an editable setting (see [references/editable-settings.md](references/editable-settings.md#granularity-doctrine-settings-first-static-blocks))
|
|
103
103
|
- Array-setting rows keyed by **index**, never by a builder-editable field value
|
|
104
104
|
- Media settings that may start empty (`src: ""`) gated with a conditional render or placeholder — never an unconditional `<img src={setting.src}>`
|
|
105
|
-
- **Deploying through the MCP:** `errors: null` on a push is not proof — compare the push result's `sourceSha256` with `shasum -a 256` of the file you sent, every byte counted, trailing newline included (Softr stores exactly
|
|
106
|
-
- **Then check it in a browser.** Once the push checks pass, check rendering and behaviour in a fresh preview with saves blocked, per [references/browser-checks.md](references/browser-checks.md)
|
|
105
|
+
- **Deploying through the MCP:** `errors: null` on a push is not proof — compare the push result's `sourceSha256` with `shasum -a 256` of the file you sent, every byte counted, trailing newline included (Softr stores exactly the text that reaches it; the one-byte drift we once blamed on it was a chunked read on our side). No `\uXXXX` escapes in pushed source: they arrive decoded to the characters and the hashes differ, so write the characters themselves (with a comment saying why) or build them from code points (`String.fromCharCode(0x300)`); lone surrogates are the exception ([why](references/softr-mcp.md#unicode-escapes-come-back-decoded)). Prove deployed == disk *before* editing the same way, with `vibe_coding_block_get_code` and `includeCode: false`, so a Studio-side change is never overwritten. Fetch the full `sourceCode` only when a digest is missing or the hashes differ. Protocol in [references/softr-mcp.md → Verifying a push](references/softr-mcp.md#verifying-a-push--the-deployed-source-is-the-only-proof)
|
|
106
|
+
- **Then check it in a browser.** Once the push checks pass, check rendering and behaviour in a fresh preview with saves blocked, in the client's time zone (a check east of UTC misses date-only values that users west of it see a day early), per [references/browser-checks.md](references/browser-checks.md)
|
|
107
107
|
|
|
108
108
|
## What to Clarify
|
|
109
109
|
|
|
@@ -190,7 +190,7 @@ For advanced patterns beyond data fetching, load the relevant reference when the
|
|
|
190
190
|
| Small reusable patterns — `localStorage` cross-page state, clipboard copy button, measuring the block's own width (not the window's), clearing Softr's sticky top bar and phone tab bar, an in-block modal above Softr's bars (instead of shadcn `Dialog`) | [references/common-patterns.md](references/common-patterns.md) |
|
|
191
191
|
| Writing Airtable Automation Scripts / Scripting Extension scripts / Airtable formulas — companion to Softr blocks for cross-table cascades and computed values | [references/airtable-automations.md](references/airtable-automations.md) |
|
|
192
192
|
| The official **Softr MCP server** — Softr DB schema + full record/table/field/database CRUD (deletes included), field-level browsing of connected integrations (Airtable / Google Sheets / Notion / Supabase and more), **creating, editing, versioning, and deploying Vibe Coding blocks directly** (`vibe_coding_block_get_docs`, `vibe_coding_block_create`, ...), push verification by `sourceSha256`, app management/scaffolding, the **Softr Workflows** suite (28 tools, 418-node catalog), and **per-application MCP servers**. Tool names changed on 2026-10-01; the file carries the old → new map | [references/softr-mcp.md](references/softr-mcp.md) |
|
|
193
|
-
| **Checking a pushed block in a browser** — rendering and behaviour in a Softr preview with the agent-browser CLI (ask before installing it): the preview cookie, reaching into the block's shadow DOM through accessibility refs, measuring with `eval`, blocking and proving the save endpoint before any click, reading what a click sent, screenshots to disk, testing Custom Code header CSS | [references/browser-checks.md](references/browser-checks.md) |
|
|
193
|
+
| **Checking a pushed block in a browser** — rendering and behaviour in a Softr preview with the agent-browser CLI (ask before installing it): running it in the client's time zone (`TZ` when the session starts) and comparing shown date-only values with the stored ones, the preview cookie, reaching into the block's shadow DOM through accessibility refs, measuring with `eval`, blocking and proving the save endpoint before any click, reading what a click sent, screenshots to disk, testing Custom Code header CSS | [references/browser-checks.md](references/browser-checks.md) |
|
|
194
194
|
| Restyling Softr's **native shell — header / footer / nav / dropdowns / page background** (not a block; it's Softr chrome, done with global Custom Code CSS): stable selectors vs. hashed classes, floating "island" header+footer, the dropdown blank-space grid fix, the multi-layer page-background stacking, restyle-vs-replace, and the **app frame** for sidebar apps (top bar + sidebar as one frame in the theme colour, the content as one paper sheet with a pinned rounded corner, blocks transparent, scoped with `:has()` so Log in / 404 keep Softr's colours) | [references/native-chrome-styling.md](references/native-chrome-styling.md) |
|
|
195
195
|
| Adding a **dynamic date filter or custom filter control to a native List/Grid block** (via a Custom Code Static block, not a Vibe block): drive the block's conditional filter with `{URL_PARAM:…}`, the empty-param "match nothing" wide-range sentinel, inject the control into the filter row and keep it alive across Softr's re-renders | [references/native-block-filters.md](references/native-block-filters.md) |
|
|
196
196
|
| **Editable settings deep-dive** — full hook catalog (incl. verified-undocumented `useLongTextSetting` and the `navigation` array-schema type), settings-first granularity doctrine, heading-line-split and `-text`/`-link` pairing patterns, naming conventions, rename-resets-value gotcha, empty-media gating, key-by-index rule | [references/editable-settings.md](references/editable-settings.md) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "softr-vibe-coding",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.2",
|
|
4
4
|
"description": "Claude Code skill for generating production-ready Softr Vibe Coding blocks (JSX). Installs into ~/.claude/skills/ and auto-updates on each Claude Code session.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"softr-vibe-coding": "bin/cli.js"
|
|
@@ -4,7 +4,8 @@ How to check a deployed block's rendering and behaviour in a Softr preview with
|
|
|
4
4
|
[agent-browser](https://github.com/vercel-labs/agent-browser) CLI. **Verified 2026-10-01** with
|
|
5
5
|
agent-browser v0.38.1 on macOS (Node 22) against a real Softr preview; only the commands under
|
|
6
6
|
[Untested but promising](#untested-but-promising) were not run. [Testing Custom Code header
|
|
7
|
-
CSS](#testing-custom-code-header-css) was verified 2026-10-05, except where it says otherwise
|
|
7
|
+
CSS](#testing-custom-code-header-css) was verified 2026-10-05, except where it says otherwise, and
|
|
8
|
+
[the client's time zone](#1-the-clients-time-zone) on 2026-10-08.
|
|
8
9
|
|
|
9
10
|
## When to use it
|
|
10
11
|
|
|
@@ -55,13 +56,54 @@ even Slack ones. Yet it is only a stub that loads `agent-browser skills get core
|
|
|
55
56
|
|
|
56
57
|
## The recipe
|
|
57
58
|
|
|
58
|
-
### 1.
|
|
59
|
+
### 1. The client's time zone
|
|
60
|
+
|
|
61
|
+
Run every check in the time zone of the app's users, never the machine's. A headless Chrome takes
|
|
62
|
+
the zone of the computer it runs on, and a date-only value arrives as midnight UTC
|
|
63
|
+
([fields.md → Date-only fields arrive as midnight UTC](../datasources/fields.md#date-only-fields-arrive-as-midnight-utc)):
|
|
64
|
+
a block that parses it with `new Date()` or `parseISO` shows the right day east of UTC and the day
|
|
65
|
+
before west of it. On 2026-10-08 three blocks of an app for Oregon had passed checks run in
|
|
66
|
+
Europe/Athens (UTC+3) while showing their users in America/Los_Angeles every date-only value a day
|
|
67
|
+
early. A test-data load found it, not the checks.
|
|
68
|
+
|
|
69
|
+
agent-browser has no time-zone setting (v0.38.1: no flag, and `set` offers none). Chrome takes its
|
|
70
|
+
zone from `TZ` in the environment of the daemon that launches it, so put `TZ` in the `ab` function:
|
|
71
|
+
then the command that starts the daemon carries it, whichever one that is. Check the zone straight
|
|
72
|
+
after launch:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
ab() { TZ=America/Los_Angeles agent-browser --session softr-check "$@"; } # the client's IANA zone
|
|
76
|
+
ab open 'about:blank' >/dev/null
|
|
77
|
+
ab eval "Intl.DateTimeFormat().resolvedOptions().timeZone + ' ' + new Date().getTimezoneOffset()"
|
|
78
|
+
# "America/Los_Angeles 420" (minutes behind UTC: 420 in summer time, 480 in winter)
|
|
79
|
+
# "Europe/Athens -180" = no TZ reached the daemon: the machine's zone
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- **`TZ` counts only when the session's daemon starts.** Against a daemon already running,
|
|
83
|
+
`TZ=America/Los_Angeles agent-browser … eval` still printed `Europe/Athens`. And `ab close` alone
|
|
84
|
+
is not enough: the daemon outlives it by about a second, and an `open` in that second came back in
|
|
85
|
+
the old zone. To change zone, close, wait until `agent-browser session list` no longer shows the
|
|
86
|
+
session, then open again; or use a new session name.
|
|
87
|
+
- **Which zone:** the client's, from the project notes, as an IANA name (`America/Los_Angeles`,
|
|
88
|
+
`America/New_York`). If the users span zones, run the date check ([step 5](#5-date-only-values-against-the-stored-ones))
|
|
89
|
+
in each.
|
|
90
|
+
- **Verified 2026-10-08** with agent-browser 0.38.1 on macOS: `about:blank`, `example.com` and a new
|
|
91
|
+
tab all reported `America/Los_Angeles` and 420, and a mock block rendering
|
|
92
|
+
`new Date("2026-10-05T00:00:00.000Z")` showed Oct 5 in Athens and Oct 4 in Los Angeles. The zone
|
|
93
|
+
belongs to the browser, not the page, so it holds on a Softr preview too (inferred, not run
|
|
94
|
+
there).
|
|
95
|
+
- **Other browsers.** With Playwright (the fallback under [Install](#install)), pass
|
|
96
|
+
`timezoneId: 'America/Los_Angeles'` to `browser.newContext()` (its documented option; not run
|
|
97
|
+
here). An in-app Browser pane or the user's own Chrome runs in its machine's zone unless
|
|
98
|
+
something overrides it: run the probe there before trusting any date it shows.
|
|
99
|
+
|
|
100
|
+
### 2. Session, preview cookie, page
|
|
59
101
|
|
|
60
102
|
Work from a scratch directory, not the project: nothing is written to the working folder, and
|
|
61
103
|
screenshots go where you tell them.
|
|
62
104
|
|
|
63
105
|
```bash
|
|
64
|
-
ab() { agent-browser --session softr-check "$@"; } # a function, not a variable: see Gotchas
|
|
106
|
+
ab() { TZ=America/Los_Angeles agent-browser --session softr-check "$@"; } # step 1; a function, not a variable: see Gotchas
|
|
65
107
|
ab open '<previewUrl>' >/dev/null # once per session: sets the preview cookie
|
|
66
108
|
ab set viewport 1280 900
|
|
67
109
|
ab open 'https://<subdomain>.preview.softr.app/<page>?recordId=<recordId>&autoUser=true'
|
|
@@ -73,7 +115,7 @@ ab wait 2500 # 2000–3000 ms more, so the block's dat
|
|
|
73
115
|
this one carries a sign-in token, hence `/dev/null`. The direct URL then loads the app itself, not
|
|
74
116
|
the toolbar shell that frames it, so `document` in `eval` is the app's.
|
|
75
117
|
|
|
76
|
-
###
|
|
118
|
+
### 3. Reaching into the block
|
|
77
119
|
|
|
78
120
|
A block renders inside a shadow root, which CSS selectors and `find` locators do not cross.
|
|
79
121
|
**Refs from the accessibility readout do**: `ab snapshot -i` lists the block's textboxes, buttons
|
|
@@ -84,7 +126,7 @@ the ref out in the same shell call, so the readout never enters your context:
|
|
|
84
126
|
REF=$(ab snapshot -i | grep -o 'textbox "Search by[^[]*\[ref=e[0-9]*' | head -1 | grep -o 'e[0-9]*$'); ab fill "@$REF" 'term'
|
|
85
127
|
```
|
|
86
128
|
|
|
87
|
-
###
|
|
129
|
+
### 4. Measuring with `eval`
|
|
88
130
|
|
|
89
131
|
`ab eval "<expr>"`, or `ab eval --stdin < check.js` for anything longer. An async IIFE is awaited,
|
|
90
132
|
and only the result is printed: return `JSON.stringify(...)` to get one JSON-encoded line. Find the
|
|
@@ -115,7 +157,44 @@ A block that uses the brand `DatePicker` ([date-picker.md](date-picker.md)) has
|
|
|
115
157
|
field is a button with a ref in `-i`. Click it, then click the day by its ref; each day is a button
|
|
116
158
|
named like `Thursday, October 15, 2026`.
|
|
117
159
|
|
|
118
|
-
###
|
|
160
|
+
### 5. Date-only values against the stored ones
|
|
161
|
+
|
|
162
|
+
On every block that shows a date-only field, compare a few shown days with the stored ones, in
|
|
163
|
+
the client's zone ([step 1](#1-the-clients-time-zone)):
|
|
164
|
+
|
|
165
|
+
1. Read three or more records with the MCP's `database_list_records` and note each date-only value.
|
|
166
|
+
Its first ten characters are the stored day: `"2026-10-05T00:00:00.000Z"` is 5 October.
|
|
167
|
+
2. Read the same records on the page, by a value that names each one (`innerText` keeps a space
|
|
168
|
+
between table cells; `textContent` runs them together):
|
|
169
|
+
|
|
170
|
+
```js
|
|
171
|
+
(() => {
|
|
172
|
+
const root = [...document.querySelectorAll('*')].map(e => e.shadowRoot).filter(Boolean)
|
|
173
|
+
.find(s => /<text unique to the block>/.test(s.textContent));
|
|
174
|
+
if (!root) return 'block not found';
|
|
175
|
+
const rows = [...root.querySelectorAll('tr, li, [role="row"]')]; // the block's row element
|
|
176
|
+
return JSON.stringify(['<name 1>', '<name 2>', '<name 3>'].map(n => {
|
|
177
|
+
const row = rows.find(e => e.textContent.includes(n));
|
|
178
|
+
return row ? row.innerText.replace(/\s+/g, ' ').trim().slice(0, 160) : n + ': not found';
|
|
179
|
+
}));
|
|
180
|
+
})()
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
3. Each shown day must be the stored day. Every one a day early means the block parses date-only
|
|
184
|
+
values with `new Date()` or `parseISO`: switch it to `toLocalDate`
|
|
185
|
+
([fields.md](../datasources/fields.md#date-only-fields-arrive-as-midnight-utc)).
|
|
186
|
+
|
|
187
|
+
- **A zone west of UTC is what exposes the midnight-UTC bug.** Midnight UTC on 5 October is still
|
|
188
|
+
5 October anywhere at or east of UTC, and 4 October at 5 pm in Los Angeles. A check in Athens
|
|
189
|
+
passes a block that is wrong for every user west of UTC, as three blocks did (step 1).
|
|
190
|
+
- **Check each place the block shows the date:** the list, the detail view, and the value an edit
|
|
191
|
+
form opens with. A form that opens a day early can save the wrong day when the user only changes
|
|
192
|
+
another field.
|
|
193
|
+
- **A "today" taken as the UTC day is wrong only part of the day.**
|
|
194
|
+
`new Date().toISOString().slice(0, 10)` is tomorrow from 5 pm in Los Angeles (4 pm in winter), so
|
|
195
|
+
a morning check misses it: look for it in the source instead.
|
|
196
|
+
|
|
197
|
+
### 6. Block saves before any click, and prove it
|
|
119
198
|
|
|
120
199
|
The preview writes to the live data
|
|
121
200
|
([softr-mcp.md](softr-mcp.md#testing-as-any-app-user-without-logins--the-preview-as-switcher)), so
|
|
@@ -139,9 +218,9 @@ Only this update endpoint is verified. Before clicking a create or delete contro
|
|
|
139
218
|
with `ab network requests` where a write is harmless, never on client data, and block that pattern
|
|
140
219
|
too. Do not assume `*records-trigger*` covers it.
|
|
141
220
|
|
|
142
|
-
###
|
|
221
|
+
### 7. Click, then read what it sent
|
|
143
222
|
|
|
144
|
-
Read the record through the database API, click Save by its ref (step
|
|
223
|
+
Read the record through the database API, click Save by its ref (step 3), then read the record
|
|
145
224
|
again: `updatedAt` and the field should be unchanged. The aborted request is still logged, so you
|
|
146
225
|
see the payload without it reaching the server:
|
|
147
226
|
|
|
@@ -150,7 +229,7 @@ ab network requests --filter records-trigger # lists the save, with its id
|
|
|
150
229
|
ab network request <id> --json # method: PATCH, postData: {"context":{…},"fields":{"<fieldId>":"2026-10-15"}}
|
|
151
230
|
```
|
|
152
231
|
|
|
153
|
-
###
|
|
232
|
+
### 8. Screenshots and cleanup
|
|
154
233
|
|
|
155
234
|
```bash
|
|
156
235
|
ab screenshot ./empty-1280.png # ✓ Screenshot saved to … (--full for the whole page)
|
|
@@ -177,7 +256,7 @@ a test surface: header code is not known to render there.
|
|
|
177
256
|
|
|
178
257
|
### 1. Before pasting: inject it into the preview
|
|
179
258
|
|
|
180
|
-
Open the page as in [step
|
|
259
|
+
Open the page as in [step 2](#2-session-preview-cookie-page), as the user whose navigation you are
|
|
181
260
|
styling: on the preview origin run `fetch('/studio/impersonate/<softrUserId>')`, then open the page
|
|
182
261
|
again ([how](softr-mcp.md#testing-as-any-app-user-without-logins--the-preview-as-switcher)). Then
|
|
183
262
|
inject the file exactly as it will be pasted, tagged so that a re-run replaces it:
|
|
@@ -353,7 +353,7 @@ no discard prompt, focus is on the date field's trigger); the second Escape reac
|
|
|
353
353
|
inside the modal: focus lands on the next field, not on the modal panel (lesson 6).
|
|
354
354
|
|
|
355
355
|
**4. The saved value is byte-identical to the native version's.** Block saves first
|
|
356
|
-
([browser-checks.md → Block saves before any click, and prove it](browser-checks.md#
|
|
356
|
+
([browser-checks.md → Block saves before any click, and prove it](browser-checks.md#6-block-saves-before-any-click-and-prove-it)),
|
|
357
357
|
pick a day, save, and read the aborted request's payload: the field holds the same string the native
|
|
358
358
|
field sent for that day (`"2026-10-15"`, not a timestamp). Clear sends what an emptied native field
|
|
359
359
|
sent, unless the block deliberately changed it (one LCDB block now saves a cleared optional date as
|
package/references/softr-mcp.md
CHANGED
|
@@ -280,7 +280,7 @@ reports neither field. With `includeCode: false`, `vibe_coding_block_get_code` r
|
|
|
280
280
|
`sourceCode: null`. Verified live 2026-10-01: a deployed block's `sourceSha256` equalled the SHA-256
|
|
281
281
|
of the exact source last pushed to it (taken from the push call), and the read came back at about
|
|
282
282
|
1 KB for a 15 KB block. The same check showed that block's local mirror had picked up three comment
|
|
283
|
-
edits since that push, which is exactly what step 1 below exists to catch. (The digest on push results
|
|
283
|
+
edits since that push, which is exactly what step 1 below exists to catch. (The digest on push results was first known from Softr's release notes; our own pushes have returned it since at least 2026-10-08.)
|
|
284
284
|
Right after that release our client's copy of the tool definition did not declare `includeCode`, so
|
|
285
285
|
the argument went out as the string `"false"` and the server still honoured it. Whatever the loaded
|
|
286
286
|
definition says, check that `sourceCode` came back `null`.
|
|
@@ -308,10 +308,12 @@ definition says, check that `sourceCode` came back `null`.
|
|
|
308
308
|
|
|
309
309
|
Matching hashes prove what Softr stored, not how the block behaves: for that, check it in a fresh preview with saves blocked, per [browser-checks.md](browser-checks.md).
|
|
310
310
|
|
|
311
|
-
**Hash the exact bytes, trailing newline included.** Softr stores exactly
|
|
311
|
+
**Hash the exact bytes, trailing newline included.** Softr stores exactly the text that reaches it: across
|
|
312
312
|
112 push→fetch pairs between 2026-09-09 and 2026-09-30 the fetched
|
|
313
313
|
`sourceCode` was byte- and MD5-identical to the text sent, including two pushes sent *without* a
|
|
314
|
-
final newline and stored without one.
|
|
314
|
+
final newline and stored without one. (One thing does not reach Softr as written: a `\uXXXX`
|
|
315
|
+
escape in the source arrives decoded, see [below](#unicode-escapes-come-back-decoded).)
|
|
316
|
+
The "deployed block is one byte shorter" we chased on
|
|
315
317
|
2026-09-09 was our own read: an agent that reads a large file in chunks can drop the final
|
|
316
318
|
newline (or a blank line at a chunk boundary) before transmission. A comparison that normalises
|
|
317
319
|
the trailing newline hides exactly that class of error — so do not normalise anything; a mismatch
|
|
@@ -356,6 +358,41 @@ from the docs):
|
|
|
356
358
|
(`application_get`) with the version's `createdAt` (`vibe_coding_block_list_versions`): on
|
|
357
359
|
2026-09-01 a block we believed staged went live with a publish 28 minutes after it was saved.
|
|
358
360
|
|
|
361
|
+
#### Unicode escapes come back decoded
|
|
362
|
+
|
|
363
|
+
Verified 2026-10-08 on a large dashboard block pushed in stages with `vibe_coding_block_update_code`
|
|
364
|
+
and `vibe_coding_block_update_code_search_replace`: exactly the calls whose text held a JavaScript
|
|
365
|
+
`\uXXXX` escape came back with a `sourceSha256` that differed from the hash of the planned text. The
|
|
366
|
+
block's accent-folding regex, `/[\u0300-\u036f]/g`, was stored with both escapes replaced by the
|
|
367
|
+
characters themselves, two combining marks sitting raw between the brackets. `\d`, `\D` and `\s` in
|
|
368
|
+
the same block arrived as written.
|
|
369
|
+
|
|
370
|
+
The decoding happens somewhere between the tool call and storage (the client's JSON handling of
|
|
371
|
+
the argument, or the server), not in Softr's compiler: the stored source text itself changed. It
|
|
372
|
+
looks like the [encoding trap](#which-edit-tool-full-replace-vs-targeted-search-replace) above,
|
|
373
|
+
reaching a backslash-u that was meant to stay in the source. The exception is a lone surrogate:
|
|
374
|
+
`\udc00-\udfff` in the same regexes has no character form, and those pushes stored it unchanged.
|
|
375
|
+
(The `\u{…}` form is untested; treat it the same.)
|
|
376
|
+
|
|
377
|
+
**So: no `\uXXXX` in pushed source, lone surrogates aside.** Find them before a push with
|
|
378
|
+
`grep -n '\\u[0-9a-fA-F]\{4\}' <file>`, then either:
|
|
379
|
+
|
|
380
|
+
- **Write the characters themselves**, with a comment saying why, since combining marks and other
|
|
381
|
+
invisible characters cannot be read in an editor. Spell "backslash-u" out in that comment so it
|
|
382
|
+
holds no escape either. Check first that the character means the same raw in that spot: a range
|
|
383
|
+
of combining marks in a character class does (the block's regexes, escaped and raw, matched the
|
|
384
|
+
same set across all 65,536 BMP code points), but written raw, a `/` ends a regex literal, a `]`
|
|
385
|
+
closes a class, a backslash or quote changes the token, and U+2028/U+2029 are line terminators a
|
|
386
|
+
regex literal may not contain.
|
|
387
|
+
- **Build it from code points** where a raw character is unwanted or unsafe:
|
|
388
|
+
`new RegExp("[" + String.fromCharCode(0x300) + "-" + String.fromCharCode(0x36f) + "]", "g")`,
|
|
389
|
+
created once at module scope. No backslash-u reaches the tool, so there is nothing to decode.
|
|
390
|
+
Strings likewise: `String.fromCharCode(0x2014)`, not `"\u2014"`.
|
|
391
|
+
|
|
392
|
+
If a push has already stored the decoded form, applying the same replacement to the mirror brings
|
|
393
|
+
the hashes back together; confirm the raw characters behave the same (first bullet) before calling
|
|
394
|
+
it done.
|
|
395
|
+
|
|
359
396
|
### The array-argument rejection, and why it is a security issue
|
|
360
397
|
|
|
361
398
|
**Several workspace-server tools take an array argument, and a call that sends it as a JSON *string*
|