Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion client/src/components/CmdKSearch.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { recordManualLayoutPick } from '../utils/timeWindow.js';
import { RECENT_KEY, RECENT_CAP, resolveRecentNavEntries } from '../utils/navWorkingSet.js';
import { filterNavByFeatures } from '../lib/navFeatures.js';
import { safeReadJsonStorage } from '../lib/safeStorage.js';
import { escapeRegExp } from '../lib/textUtils.js';

const ICON_MAP = { Brain, Cpu, Package, History, HeartPulse };

Expand Down Expand Up @@ -46,7 +47,7 @@ const precompute = (cmd) => ({

function Highlight({ text, query }) {
if (!query || !text) return <span>{text}</span>;
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escaped = escapeRegExp(query);
const parts = text.split(new RegExp(`(${escaped})`, 'gi'));
return (
<span>
Expand Down
11 changes: 5 additions & 6 deletions client/src/hooks/mountedRefConventions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import { readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { trackedSourceFiles } from '../test/trackedFiles.js';
import { escapeRegExp } from '../lib/textUtils.js';

const CLIENT_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');

Expand All @@ -88,14 +89,12 @@ const HOOK_FILE = 'src/hooks/useMounted.js';
// correct) pattern, so it is intentionally out of scope.
const TRUE_SEEDED_REF = /const\s+([A-Za-z_$][\w$]*)\s*=\s*useRef\(\s*true\s*\)/g;

// Ref names may legally contain `$`, which is a regex anchor — interpolating one
// raw would silently make the pattern unmatchable and pass the offender.
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

// `=` and `||=` both count as re-arming. Matching only `=` would report a ref
// re-armed with `ref.current ||= true` as broken — a false positive that would
// push someone to "fix" already-correct code.
const assignsRe = (name, value) => new RegExp(`\\b${escapeRe(name)}\\.current\\s*(?:\\|\\|)?=\\s*${value}\\b`);
// push someone to "fix" already-correct code. The name is escaped because ref
// names may legally contain `$`, which is a regex anchor — interpolating one raw
// would silently make the pattern unmatchable and pass the offender.
const assignsRe = (name, value) => new RegExp(`\\b${escapeRegExp(name)}\\.current\\s*(?:\\|\\|)?=\\s*${value}\\b`);

const USE_EFFECT_OPEN = /\buseEffect\s*\(/g;

Expand Down
1 change: 1 addition & 0 deletions client/src/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ grep -i "what you want to do" client/src/lib/README.md
| `terminalDictation.js` | Voice-dictation/IME bridge for the Shell's xterm.js terminal. Apple dictation streams progressively refined guesses and *replaces* what it already typed; xterm forwards each insertion and drops the matching deletions, so the PTY accumulates a garble (`determin` + `determine` + `determines`…). `attachDictationBridge(terminal, sendData)` binds capture-phase listeners on `terminal.element` — ahead of xterm's own textarea listeners, which it stops — and forwards a diff instead: `TERMINAL_DEL` (`0x7f`) per dropped character, then the added text. `sendData` returns `false` to report a dropped send, which leaves the mirror where the PTY actually is. Paths that reach the PTY some other way (a keystroke xterm handles itself, paste, blur, composition end, screen-reader mode) resync the mirror without emitting. `planFieldEdit(mirror, next, floor)` → `{ data, committed }` is the pure core: `floor` is the prefix we did not write and must never rewind through, and `committed` (what the PTY holds afterward, which is *not* the field's value when the floor blocked a rewind) is what the caller must track. The prefix scan never splits a surrogate pair — cutting between the halves of an astral character would send a lone surrogate that serializes to `U+FFFD`. Wired in `useShellSession`; the test suite pins the seam against a real `Terminal`. |
| `terminalScroll.js` | Scrolling the Shell terminal by touch, wheel, and page controls. xterm 6 binds **no touch handlers at all**, and a TUI’s ALTERNATE screen buffer has no terminal scrollback, so `scrollLines()` clamps to a no-op there. Normal shell scrollback uses `scrollLines()`; alternate-screen wheel gestures are captured before xterm’s mouse listener and translated to standard PageUp/PageDown input, which OpenCode supports for its message viewport, while apps that explicitly enable terminal mouse tracking keep xterm’s native wheel path. `attachTerminalTouchScroll(terminal)` (→ detach fn) makes a one-finger drag scroll in either buffer, `attachTerminalWheelScroll(terminal)` handles native wheel input, and `scrollTerminalPage(terminal, direction)` powers the `SCROLL_KEYS` buttons. `measureTerminalGeometry(terminal)` takes one layout read per gesture; `planTouchScrollSteps(accumPx, rowHeightPx)` is the pure sub-row-remainder core. Wired in `useShellSession`. |
| `terminalTheme.js` | Pure xterm.js palette builder for the Shell terminal. `buildTerminalTheme({ bg, fg, accent, card, error, success, warning }, mode)` assembles the xterm `theme` object — base ANSI colors from the active theme's CSS vars, the rest from mode-tuned literals (`ANSI_NIGHT` bright/pastel for dark backgrounds, `ANSI_DAY` darkened/saturated so colored CLI output stays legible on daytime themes). `parseCssColorToHex(raw, fallback)` normalizes both the `15 15 15` triple form and the `rgb(7 7 7 / 0.86)` function form (used by `--port-terminal-*` tokens) to `#rrggbb`, dropping alpha. No DOM reads — `Shell.jsx` resolves CSS vars and re-applies on theme switch. |
| `textUtils.js` | Mirror of `server/lib/textUtils.js` (the `escapeRegExp` half — the only member the bundle has a caller for). `escapeRegExp(value)` is the one client-side RegExp escape: import it instead of re-inlining the character class, which a guard in `server/lib/textUtils.test.js` now fails the suite over on either side of the mirror. Coerces non-strings rather than throwing, matching the server. |
| `threejsAnimation.js` | Pure pose evaluator for the declarative clip contract in `server/lib/threejsModel.js`, used by the Three.js Models preview transport. `evaluateThreejsClipPose(clip, timeSeconds)` → `{ timeSeconds, pose, activeSequenceIds, activePartIds }` where `pose` is a null-prototype partId → `{ position?, rotationDegrees?, scale?, opacity?, visible? }` map: each part+channel resolves from the one sequence that owns the instant (the window containing it, else the most recent behind it, else the next ahead), `visible` steps at the window's end rather than interpolating, and a part no sequence drives is absent so the spec renders as authored. `collectThreejsCues(clip, from, to)` returns the data-only sound cues a play loop crossed in the half-open `[from, to)` interval — a scrub calls it never, which is what makes scrubbing silent. `listThreejsClips` / `listThreejsCues` / `resolveThreejsClip` / `getThreejsClipDuration` read a spec that may have no `animation` key at all. |
| `threejsEnvironment.js` | Image-based lighting for the procedural sculpt spec, used by the Three.js Models preview. `spec.lights` are punctual — they light a surface but give it nothing to REFLECT, so `metalness`, `transmission`, `clearcoat` and `iridescence` read off an environment or read off nothing, and a plausible conductor renders near-black without one. `createSculptEnvironmentTarget(renderer, preset)` builds a preset locally from three's own primitives and prefilters it through `PMREMGenerator` — `neutral` is three's bundled `RoomEnvironment`, `studio` a dark shell with three emissive softbox panels plus a floor bounce — returning null for `none`; the caller owns the render TARGET and disposes it (disposing only `.texture` leaks the framebuffer behind it). `THREEJS_RENDER_PROFILE` mirrors the server's colour-space/tone-map/exposure contract so the preview renders at what the export stamps on the model. Deliberately NOT drei's `<Environment preset=…>`, which fetches an HDR from a CDN: rendering a local model makes no outbound request. `resolveSculptEnvironment(spec)` is the client mirror of `resolveThreejsEnvironment` in `server/lib/threejsModel.js`, reading a spec with no `environment` key as the `none` it was actually authored against. |
| `threejsExplode.js` | Disassembly + part-picking maths for the procedural sculpt spec, shared by the Three.js Models preview so explode and the picker agree on what "a part" is. `computeExplodeLayout(parts, amount)` → `{ offsets, meshOffsets, unitIds, growth }`: separation is a layout **scale about the model centre** (≈2× at full explode) plus a base clearance for parts sitting at the centre — never a uniform outward push, which slides the arrangement without opening gaps — where `offsets` add to a unit's own `position` and `meshOffsets` apply to a group around a container's own geometry (moving its group instead would drag its child units along), and `growth` measures how much the layout actually grew so the camera re-fits on real change. `buildPartSelectionIndex(parts)` → `{ owners, ancestry, names }` for click resolution, subtree highlight, and the selection label. All maps are null-prototype — part ids are provider-authored and `idSchema` accepts `toString`. `isReliefPart` / `isContainerPart` expose the shared part definition: a part flagged `explodeWithParent` is surface relief that rides its parent and resolves selection up to it (unless it has no parent to ride), every other geometry-bearing part is both a movable unit and a selectable component, a part whose descendants carry geometry is additionally descended through, and a part with no geometry in its subtree moves nothing. |
Expand Down
3 changes: 1 addition & 2 deletions client/src/lib/editorialChecks.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// findings triage grouping, and manuscript deep-link building. No React, no
// window: the page component and its unit tests both consume these.
import { descriptorForCanonEntry } from './canonPrompt.js';
import { escapeRegExp } from './textUtils.js';

// Scope display order + labels (mirrors server CHECK_SCOPES). A check whose
// scope isn't one of these still renders, bucketed under its raw scope last.
Expand Down Expand Up @@ -472,8 +473,6 @@ export function canonEntitiesFromUniverse(universe) {
return out;
}

const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

// Build a single case-insensitive, word-boundary-anchored matcher over every
// canon name (longest first so "Jon Snow" wins over "Jon"), plus a lowercase
// name→entity map. Returns `null` when there are no usable names.
Expand Down
1 change: 1 addition & 0 deletions client/src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export * from './tailnetPeer.js';
export * from './terminalDictation.js';
export * from './terminalScroll.js';
export * from './terminalTheme.js';
export * from './textUtils.js';
export * from './threejsAnimation.js';
export * from './threejsEnvironment.js';
export * from './threejsExplode.js';
Expand Down
4 changes: 2 additions & 2 deletions client/src/lib/loraTriggers.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
* identical to the server module or the picker's hint will contradict the render.
*/

import { escapeRegExp } from './textUtils.js';

// Only the FIRST trigger word of a LoRA activates it, per the server weave —
// Civitai `trainedWords` routinely lists a dozen loosely-related tags.
export const firstTriggerWord = (words) => {
Expand All @@ -17,8 +19,6 @@ export const firstTriggerWord = (words) => {
return first ? first.trim() : null;
};

const escapeRegExp = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

// What counts as "inside a word" for the boundary assertions below. Unicode
// letters/digits, not just ASCII, so a non-ASCII trigger or an accented prompt
// gets the same treatment — `\b` and a bare `[A-Za-z0-9_]` class would both
Expand Down
4 changes: 3 additions & 1 deletion client/src/lib/manuscriptAnchors.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
* carrying every comment covering it, toned by the highest severity present.
*/

import { escapeRegExp } from './textUtils.js';

const SEVERITY_RANK = { high: 3, medium: 2, low: 1 };

// Locate the `find` span to highlight/replace. `indexOf` alone targets the FIRST
Expand Down Expand Up @@ -49,7 +51,7 @@ export function locateFindSpan(text, find, anchorQuote) {
const exact = locateFind(text, find, anchorQuote);
if (exact !== -1) return { start: exact, end: exact + find.length };

const escaped = find.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escaped = escapeRegExp(find);
const re = new RegExp(escaped.replace(/\s+/g, '\\s+'), 'g');
const anchorIdx = anchorQuote ? text.indexOf(anchorQuote) : -1;
let best = null;
Expand Down
3 changes: 2 additions & 1 deletion client/src/lib/scenePrompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The shape-invariant tests in server/lib/scenePrompt.test.js are the contract.

import { mapCanonDescriptorFragments, richCanonDescriptorFragments } from './canonPrompt.js';
import { escapeRegExp } from './textUtils.js';

const PROMPT_MAX = 1900;

Expand Down Expand Up @@ -63,7 +64,7 @@ function matchEntriesByCandidates(text, entries, candidatesFn) {
const seen = new Set();
const wordBoundary = (needle) => {
if (!needle) return false;
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escaped = escapeRegExp(needle);
// Unicode-aware boundary instead of ASCII `\b`: a name starting/ending with a
// non-ASCII letter (José, Élodie, Zoë) has no `\b` adjacent to the accented
// char, so `\b…\b` would silently miss it. Lookarounds over `[\p{L}\p{N}_]`
Expand Down
26 changes: 26 additions & 0 deletions client/src/lib/textUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Mirror of server/lib/textUtils.js — the client-side home for the RegExp
// escape. Partial by design: only `escapeRegExp` is mirrored, because it is the
// only member the browser bundle has a caller for. `countWords` already mirrors
// through `client/src/utils/formatters.js`, and `trimTo`/`kebabCase` have no
// client caller — adding them here would ship dead bytes and invite drift on
// helpers nothing checks. The server copy is authoritative; the parity pin in
// `server/lib/textUtils.test.js` is the contract.
//
// It exists because the browser cannot import from `server/`, so before this
// module every client caller re-inlined the character class — the exact rot the
// server-side guard closed on its own tree (#5790). That guard now scans
// `client/src` too, so a fresh private copy on this side fails the suite.

/**
* Escape a string for literal use inside a RegExp.
*
* This is the ONE client copy — import it, never re-inline the character class.
*
* Non-string input is coerced rather than throwing, matching the server: the
* callers escape user-supplied tokens (LoRA trigger words, canon character
* names, ⌘K queries) on the way into `new RegExp(...)`, where a TypeError would
* blank a rendered surface instead of simply not matching.
*/
export function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
5 changes: 3 additions & 2 deletions client/src/pages/LoraDatasetDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import TrainingPanel from '../components/loraTraining/TrainingPanel';
import CaptionModelPicker from '../components/loraTraining/CaptionModelPicker';
import ImportGalleryDialog from '../components/loraTraining/ImportGalleryDialog';
import UniverseCharacterPicker from '../components/loraTraining/UniverseCharacterPicker';
import { escapeRegExp } from '../lib/textUtils.js';
import {
getLoraDataset,
getLoraDatasetVariationAxes,
Expand Down Expand Up @@ -61,7 +62,7 @@ const captionHasTriggerWord = (caption, triggerWord) => {
const text = (caption || '').trim();
if (!text) return false;
if (!word) return true;
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escaped = escapeRegExp(word);
return new RegExp(`(?:^|[^a-z0-9_])${escaped}(?:[^a-z0-9_]|$)`, 'i').test(text);
};
// Mirror of server/lib/loraDataset.js analyzeCaptionInvariants — flags the
Expand All @@ -76,7 +77,7 @@ const captionBody = (caption, triggerWord) => {
const word = (triggerWord || '').trim();
let body = (caption || '').trim();
if (word) {
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escaped = escapeRegExp(word);
body = body.replace(new RegExp(`^${escaped}(?=[\\s,]|$)\\s*,?\\s*`, 'i'), '');
}
return body;
Expand Down
2 changes: 1 addition & 1 deletion server/lib/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ pm` default, `NPM_CONFIG_PREFIX`, nvm/Volta) installed `codex` successfully and
| `navManifest.js` | Single source of truth for nav (`⌘K` palette + voice). Add an entry when you add a page. |
| `noReplaceMove.js` | `moveWithoutReplace(from, to)` — publish a staged file into its final name WITHOUT ever clobbering an existing one. `fs.rename` silently replaces its destination, which is the wrong default for a derived artifact; this uses `link(2)` + `unlink(2)`, so an existing destination fails atomically with `MOVE_DEST_EXISTS` and both files survive. Refuses rather than degrading when the filesystem cannot express it (`MOVE_CROSS_DEVICE`, `MOVE_NO_REPLACE_UNSUPPORTED`) — a `stat`-then-`rename` fallback would be a race. Used by the rigging publication contract (`services/rigging/autoSkin.js`). |
| `personaTraitBlend.js` | Digital-twin persona trait-blending (M34 P7). Blends a persona's `traitAdjustments` against the base twin's communication profile + Big-Five into a "Communication Calibration" directive. Mirrored to `client/src/lib/`. |
| `textUtils.js` | Pure dependency-free prose helpers. `countWords(text)` is the canonical whitespace-token count (`\S+`); `trimTo(value, max)` trims and bounds strings without coercing non-strings, and is safe for shared modules consumed by the browser; `escapeRegExp(value)` is the one to import instead of re-inlining the escape (a guard in `textUtils.test.js` fails the suite when a copy reappears in any non-test source under `server/`); `kebabCase(text)` is the canonical ASCII slug transform (PLAN.md `[slug]` ids and `planner:<model>` labels). |
| `textUtils.js` | Pure dependency-free prose helpers. `countWords(text)` is the canonical whitespace-token count (`\S+`); `trimTo(value, max)` trims and bounds strings without coercing non-strings, and is safe for shared modules consumed by the browser; `escapeRegExp(value)` is the one to import instead of re-inlining the escape (a guard in `textUtils.test.js` fails the suite when a copy reappears in any non-test source under `server/`, or in ANY source under `client/src/`, tests and `.jsx` included — the escape half is mirrored on the client at `client/src/lib/textUtils.js`, which the browser imports since it cannot reach `server/lib`); `kebabCase(text)` is the canonical ASCII slug transform (PLAN.md `[slug]` ids and `planner:<model>` labels). |
| `pipelineIssueOrder.js` | Pure renumber algorithm for pipeline issues. |
| `postAdaptive.js` | Pure POST adaptive-difficulty policy — nudges a math drill's primary knob (`steps`/`maxDigits`/`maxExponent`/`tolerancePct`) up/down within clamped bounds from recent scored performance. Opt-in via the config Adaptive toggle. |
| `postAppliedNumeracy.js` | Pure seeded Applied Numeracy pack — everyday percentage, ratio, unit, rate, and estimation scenarios plus server-authoritative numeric/fraction/unit scoring with explicit tolerance handling. |
Expand Down
3 changes: 2 additions & 1 deletion server/lib/scenePrompt.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*/

import { mapCanonDescriptorFragments, richCanonDescriptorFragments } from './canonPrompt.js';
import { escapeRegExp } from './textUtils.js';

const PROMPT_MAX = 1900;

Expand Down Expand Up @@ -78,7 +79,7 @@ function matchEntriesByCandidates(text, entries, candidatesFn) {
const seen = new Set();
const wordBoundary = (needle) => {
if (!needle) return false;
const escaped = needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escaped = escapeRegExp(needle);
// Unicode-aware boundary instead of ASCII `\b`: a name starting/ending with a
// non-ASCII letter (José, Élodie, Zoë) has no `\b` adjacent to the accented
// char, so `\b…\b` would silently miss it. Lookarounds over `[\p{L}\p{N}_]`
Expand Down
Loading