@cueloop/client 0.1.0-alpha.43 → 0.1.0-alpha.45
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/package.json +5 -5
- package/src/App.plan-review.test.tsx +9 -9
- package/src/App.readonly.test.tsx +3 -3
- package/src/App.submit-confirm.test.tsx +6 -8
- package/src/App.tsx +26 -21
- package/src/App.walk.test.tsx +2 -1
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/components/AnnotationCard.stories.tsx +9 -9
- package/src/components/AnnotationCard.tsx +16 -10
- package/src/components/ConfirmCard.stories.tsx +3 -24
- package/src/components/ConfirmCard.tsx +8 -16
- package/src/components/MarkerPopover.tsx +7 -1
- package/src/components/PlanSheet.tsx +115 -48
- package/src/components/ReviewRail.stories.tsx +0 -2
- package/src/components/ReviewRail.tsx +2 -7
- package/src/components/__snapshots__/stories.test.tsx.snap +9 -9
- package/src/components/agent-launcher.tsx +1 -1
- package/src/theme.ts +2 -2
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +105 -0
- package/src/view-plan.ts +96 -9
package/src/view-plan.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
inlineRuns,
|
|
11
12
|
isAddressed,
|
|
12
13
|
lcsDiff,
|
|
13
14
|
parseBlocks,
|
|
@@ -141,7 +142,18 @@ function numberOrderedListItems(display: DisplayBlock[]): void {
|
|
|
141
142
|
|
|
142
143
|
// ── marks (annotation highlights + keyboard span) ──
|
|
143
144
|
|
|
144
|
-
export type RunRole =
|
|
145
|
+
export type RunRole =
|
|
146
|
+
| "plain"
|
|
147
|
+
| "ins"
|
|
148
|
+
| "del"
|
|
149
|
+
| "mark-comment"
|
|
150
|
+
| "kspan"
|
|
151
|
+
| "mark-focus"
|
|
152
|
+
| "strong"
|
|
153
|
+
| "em"
|
|
154
|
+
| "code"
|
|
155
|
+
| "strike"
|
|
156
|
+
| "link";
|
|
145
157
|
|
|
146
158
|
export interface StyleRun {
|
|
147
159
|
text: string;
|
|
@@ -149,6 +161,8 @@ export interface StyleRun {
|
|
|
149
161
|
/** Offset of this run in the block's working text; null for del runs. */
|
|
150
162
|
start: number | null;
|
|
151
163
|
annotationId?: string;
|
|
164
|
+
/** Link target for `link` runs. */
|
|
165
|
+
href?: string;
|
|
152
166
|
}
|
|
153
167
|
|
|
154
168
|
export interface Mark {
|
|
@@ -195,6 +209,7 @@ export function marksByDisplay(
|
|
|
195
209
|
|
|
196
210
|
/** Base runs for a display block: plain text, or word-diff for mod blocks. */
|
|
197
211
|
export function blockRuns(block: DisplayBlock, markup: boolean): StyleRun[] {
|
|
212
|
+
const literal = block.kind === "code" || block.kind === "hr";
|
|
198
213
|
if (block.type === "mod" && markup) {
|
|
199
214
|
const changes = wordLevelChanges(block.base!.text, block.work!.text);
|
|
200
215
|
const runs: StyleRun[] = [];
|
|
@@ -202,18 +217,57 @@ export function blockRuns(block: DisplayBlock, markup: boolean): StyleRun[] {
|
|
|
202
217
|
for (const change of changes) {
|
|
203
218
|
if (change.kind === "removed") {
|
|
204
219
|
runs.push({ text: change.text, role: "del", start: null });
|
|
205
|
-
|
|
206
|
-
runs.push({
|
|
207
|
-
text: change.text,
|
|
208
|
-
role: change.kind === "added" ? "ins" : "plain",
|
|
209
|
-
start: workOffset,
|
|
210
|
-
});
|
|
211
|
-
workOffset += change.text.length;
|
|
220
|
+
continue;
|
|
212
221
|
}
|
|
222
|
+
// A code/rule block stays literal; prose segments inline-tokenize so their
|
|
223
|
+
// markers conceal while their work offsets stay intact. Added text keeps
|
|
224
|
+
// the green diff role (it wins over emphasis); context text shows emphasis.
|
|
225
|
+
const segment = literal
|
|
226
|
+
? [{ text: change.text, role: "plain" as const, start: workOffset }]
|
|
227
|
+
: inlineStyleRuns(change.text, workOffset);
|
|
228
|
+
for (const run of segment) {
|
|
229
|
+
runs.push(change.kind === "added" ? { ...run, role: "ins" } : run);
|
|
230
|
+
}
|
|
231
|
+
workOffset += change.text.length;
|
|
213
232
|
}
|
|
214
233
|
return runs;
|
|
215
234
|
}
|
|
216
|
-
|
|
235
|
+
const text = displayText(block);
|
|
236
|
+
// Prose carries inline markup; code fences and rules are literal. Concealed
|
|
237
|
+
// markers are dropped, so every rendered cell still maps to a work offset.
|
|
238
|
+
if (markup && !literal) {
|
|
239
|
+
return inlineStyleRuns(text, 0);
|
|
240
|
+
}
|
|
241
|
+
return [{ text, role: "plain", start: 0 }];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** A link href safe to hand a terminal as an OSC 8 target, or undefined. Only
|
|
245
|
+
* http(s) and mailto are trusted; a scheme like javascript: is dropped. The
|
|
246
|
+
* target must stay printable ASCII: a control byte (ESC, BEL) in a reviewed
|
|
247
|
+
* document could otherwise splice into the terminal escape stream. */
|
|
248
|
+
export function safeLinkHref(href: string | undefined): string | undefined {
|
|
249
|
+
if (href === undefined) return undefined;
|
|
250
|
+
if (!/^(https?:|mailto:)/i.test(href)) return undefined;
|
|
251
|
+
return /^[\x21-\x7e]+$/.test(href) ? href : undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Map the schema inline tokens to positioned StyleRuns, dropping the concealed
|
|
256
|
+
* markers. `base` is the run's offset within the block's working text (0 for a
|
|
257
|
+
* whole prose block; a segment's start for a word-diff piece).
|
|
258
|
+
*/
|
|
259
|
+
export function inlineStyleRuns(text: string, base: number): StyleRun[] {
|
|
260
|
+
const runs: StyleRun[] = [];
|
|
261
|
+
for (const run of inlineRuns(text)) {
|
|
262
|
+
if (run.role === "marker" || run.start === null) continue;
|
|
263
|
+
runs.push({
|
|
264
|
+
text: run.text,
|
|
265
|
+
role: run.role === "text" ? "plain" : run.role,
|
|
266
|
+
start: base + run.start,
|
|
267
|
+
...(run.href !== undefined ? { href: run.href } : {}),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
return runs;
|
|
217
271
|
}
|
|
218
272
|
|
|
219
273
|
/** Split runs at mark boundaries; marks only bind to runs with offsets. */
|
|
@@ -274,6 +328,39 @@ export function renderedOffsetFor(runs: StyleRun[], workOffset: number): number
|
|
|
274
328
|
return null;
|
|
275
329
|
}
|
|
276
330
|
|
|
331
|
+
/**
|
|
332
|
+
* Rendered offset of the first positioned character at or after `workOffset`.
|
|
333
|
+
* A word span may start on a concealed marker (the `**` of an emphasized
|
|
334
|
+
* word), which has no rendered cell; the span visuals snap to the first
|
|
335
|
+
* character that does. Positioned runs appear in ascending work order.
|
|
336
|
+
*/
|
|
337
|
+
export function renderedOffsetAtOrAfter(runs: StyleRun[], workOffset: number): number | null {
|
|
338
|
+
let rendered = 0;
|
|
339
|
+
for (const run of runs) {
|
|
340
|
+
if (run.start !== null && run.start + run.text.length > workOffset) {
|
|
341
|
+
return rendered + Math.max(0, workOffset - run.start);
|
|
342
|
+
}
|
|
343
|
+
rendered += run.text.length;
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Rendered offset of the last positioned character at or before `workOffset` -
|
|
350
|
+
* the closing-marker counterpart of `renderedOffsetAtOrAfter`.
|
|
351
|
+
*/
|
|
352
|
+
export function renderedOffsetAtOrBefore(runs: StyleRun[], workOffset: number): number | null {
|
|
353
|
+
let rendered = 0;
|
|
354
|
+
let best: number | null = null;
|
|
355
|
+
for (const run of runs) {
|
|
356
|
+
if (run.start !== null && run.start <= workOffset) {
|
|
357
|
+
best = rendered + Math.min(workOffset - run.start, run.text.length - 1);
|
|
358
|
+
}
|
|
359
|
+
rendered += run.text.length;
|
|
360
|
+
}
|
|
361
|
+
return best;
|
|
362
|
+
}
|
|
363
|
+
|
|
277
364
|
/**
|
|
278
365
|
* Work-text range covered by a rendered-text selection [start, end). Del runs
|
|
279
366
|
* and other unpositioned text inside the selection contribute nothing; the
|