@timurproko/a1 0.1.8-dev.572 → 0.1.8-dev.581
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/dist/app/session-shell/prompt-chips.js +20 -28
- package/dist/app/session-shell/session-shell-root.d.ts +2 -0
- package/dist/app/session-shell/session-shell-root.js +63 -14
- package/dist/app/session-shell/session-shell.js +30 -2
- package/dist/app/session-shell/session-viewport-controller.d.ts +1 -0
- package/dist/app/session-shell/session-viewport-controller.js +5 -1
- package/dist/contracts/owned-ui/index.d.ts +2 -0
- package/dist/contracts/owned-ui/index.js +1 -0
- package/dist/contracts/owned-ui/prompt-chips.d.ts +15 -0
- package/dist/contracts/owned-ui/prompt-chips.js +59 -0
- package/dist/contracts/owned-ui/validation.js +1 -1
- package/dist/integrations/pi/components/index.d.ts +1 -0
- package/dist/integrations/pi/components/index.js +1 -0
- package/dist/integrations/pi/components/modal-frame.d.ts +41 -0
- package/dist/integrations/pi/components/modal-frame.js +110 -0
- package/dist/integrations/pi/components/models-dialog.js +19 -5
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +38 -2
- package/dist/integrations/pi/components/skills-dialog.js +3 -3
- package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +1 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.js +5 -2
- package/dist/integrations/pi/components/upstream/components/extension-editor.d.ts +2 -1
- package/dist/integrations/pi/components/upstream/components/extension-editor.js +7 -6
- package/dist/integrations/pi/components/upstream/components/extension-input.js +9 -8
- package/dist/integrations/pi/components/upstream/components/extension-selector.js +10 -9
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +3 -3
- package/dist/integrations/pi/components/upstream/components/session-selector.js +13 -6
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +15 -12
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +1 -1
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +13 -6
- package/dist/integrations/pi/components/upstream/components/trust-selector.js +9 -9
- package/dist/integrations/pi/engine/compaction-progress.d.ts +2 -6
- package/dist/integrations/pi/engine/compaction-progress.js +10 -10
- package/dist/integrations/pi/engine/settings-bridge.js +1 -1
- package/dist/integrations/pi/engine/settings-port.js +1 -1
- package/dist/integrations/pi/tui-runtime/adapter.js +6 -2
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
- package/dist/integrations/pi/tui-runtime/mouse-report-input.d.ts +6 -0
- package/dist/integrations/pi/tui-runtime/mouse-report-input.js +34 -0
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/ui/components/dialog-panel.js +10 -6
- package/dist/ui/components/mouse.d.ts +2 -2
- package/dist/ui/components/mouse.js +2 -2
- package/dist/ui/components/transcript-viewport.d.ts +6 -0
- package/dist/ui/components/transcript-viewport.js +186 -27
- package/docs/architecture/ui-reference-provenance.md +9 -0
- package/docs/local-worktree-cleanup.md +3 -1
- package/docs/openspec-archive-automation.md +4 -0
- package/docs/validation.md +1 -1
- package/package.json +1 -1
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { canonicalizeClipboardImage } from "./clipboard-image.js";
|
|
4
|
-
import { assertImageEncodedSize, assertPromptImages, ImageAttachmentError } from "../../contracts/owned-ui/index.js";
|
|
4
|
+
import { assertImageEncodedSize, assertPromptImages, canonicalPromptChipMatches, ImageAttachmentError, replaceCanonicalPromptChips, } from "../../contracts/owned-ui/index.js";
|
|
5
5
|
import { ImagePreparationClient } from "./image-preparation-client.js";
|
|
6
6
|
import { preparePasteText } from "./paste-text-preparation.js";
|
|
7
7
|
import { pathChipTag } from "./path-chip-presentation.js";
|
|
8
8
|
import { PastePreparationClient } from "./paste-preparation-client.js";
|
|
9
|
-
const CHIP_PATTERN = /\[(?:paste #\d+ (?:\+\d+ lines|\d+ chars)|📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
|
|
10
9
|
const URL_SUBSTRING_PATTERN = /https?:\/\/[^\s\u0000-\u001f\u007f\]]+/giu;
|
|
11
10
|
const IMAGE_CHIP_IDENTIFIER_PATTERN = /^\[📷 screenshot-([a-f0-9]+)(?:-resized)?\]$/u;
|
|
12
11
|
const URL_DISPLAY_LENGTH = 40;
|
|
@@ -259,10 +258,9 @@ export class PromptChipStore {
|
|
|
259
258
|
return ranges.sort((left, right) => left.start - right.start);
|
|
260
259
|
}
|
|
261
260
|
atomicRanges(line) {
|
|
262
|
-
return
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}));
|
|
261
|
+
return canonicalPromptChipMatches(line)
|
|
262
|
+
.filter(match => !match.text.startsWith("[paste #") || this.#chips.has(match.text))
|
|
263
|
+
.map(match => ({ start: match.start, end: match.end }));
|
|
266
264
|
}
|
|
267
265
|
hyperlinkRanges(text) {
|
|
268
266
|
const ranges = [];
|
|
@@ -306,8 +304,8 @@ export class PromptChipStore {
|
|
|
306
304
|
imageChipAttachments(text) {
|
|
307
305
|
const results = [];
|
|
308
306
|
const seen = new Set();
|
|
309
|
-
for (const match of text
|
|
310
|
-
const tag = match
|
|
307
|
+
for (const match of canonicalPromptChipMatches(text)) {
|
|
308
|
+
const tag = match.text;
|
|
311
309
|
if (seen.has(tag))
|
|
312
310
|
continue;
|
|
313
311
|
const chip = this.#chips.get(tag);
|
|
@@ -338,39 +336,32 @@ export class PromptChipStore {
|
|
|
338
336
|
// Rationale: resolve image chip tags in-place before further classification. Sidecar hits
|
|
339
337
|
// register a live image chip; misses silently strip the tag (no placeholder, no notice) per
|
|
340
338
|
// the durability contract.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const identifier = imageChipIdentifier(tag);
|
|
339
|
+
const resolvedImages = replaceCanonicalPromptChips(text, match => {
|
|
340
|
+
const identifier = imageChipIdentifier(match.text);
|
|
344
341
|
if (identifier === null)
|
|
345
|
-
return
|
|
342
|
+
return match.text;
|
|
346
343
|
const attachment = resolveImage(identifier);
|
|
347
344
|
if (attachment === null)
|
|
348
345
|
return "";
|
|
349
|
-
this.#chips.set(
|
|
350
|
-
return
|
|
346
|
+
this.#chips.set(match.text, { kind: "image", tag: match.text, image: attachment });
|
|
347
|
+
return match.text;
|
|
351
348
|
});
|
|
352
349
|
// Rationale: split around any surviving image tags so paste-time classification runs on the
|
|
353
350
|
// intervening prose exactly as it would on a fresh clipboard payload, plus a substring URL
|
|
354
351
|
// scan so embedded links become atomic chips even in the middle of surrounding text.
|
|
355
|
-
CHIP_PATTERN.lastIndex = 0;
|
|
356
352
|
const segments = [];
|
|
357
353
|
let cursor = 0;
|
|
358
|
-
for (const match of resolvedImages
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
cursor = index + match[0].length;
|
|
354
|
+
for (const match of canonicalPromptChipMatches(resolvedImages)) {
|
|
355
|
+
if (match.start > cursor)
|
|
356
|
+
segments.push(resolvedImages.slice(cursor, match.start));
|
|
357
|
+
segments.push(match.text);
|
|
358
|
+
cursor = match.end;
|
|
364
359
|
}
|
|
365
360
|
if (cursor < resolvedImages.length)
|
|
366
361
|
segments.push(resolvedImages.slice(cursor));
|
|
367
|
-
CHIP_PATTERN.lastIndex = 0;
|
|
368
362
|
return segments.map(segment => {
|
|
369
|
-
if (segment.length === 0 ||
|
|
370
|
-
CHIP_PATTERN.lastIndex = 0;
|
|
363
|
+
if (segment.length === 0 || canonicalPromptChipMatches(segment).length > 0)
|
|
371
364
|
return segment;
|
|
372
|
-
}
|
|
373
|
-
CHIP_PATTERN.lastIndex = 0;
|
|
374
365
|
// Invariant: preserve original whitespace framing so the concatenated recall value
|
|
375
366
|
// remains stable across rehydration.
|
|
376
367
|
const leading = segment.match(/^\s+/)?.[0] ?? "";
|
|
@@ -434,7 +425,8 @@ export class PromptChipStore {
|
|
|
434
425
|
return chip.kind === "url" ? chip.url : chip.path;
|
|
435
426
|
};
|
|
436
427
|
// Invariant: scan only draft tokens (and resolved reservation tokens), never emitted payloads.
|
|
437
|
-
const expanded = text
|
|
428
|
+
const expanded = replaceCanonicalPromptChips(text, match => {
|
|
429
|
+
const tag = match.text;
|
|
438
430
|
const entry = this.#pending.get(tag) ?? this.#pending.get(tag.replace("failed-", "screenshot-"));
|
|
439
431
|
if (entry === undefined)
|
|
440
432
|
return resolveChip(tag);
|
|
@@ -442,7 +434,7 @@ export class PromptChipStore {
|
|
|
442
434
|
throw entry.error;
|
|
443
435
|
if (includeImages && entry.replacement === undefined)
|
|
444
436
|
throw new ImageAttachmentError("image-pending");
|
|
445
|
-
return entry.replacement === undefined ? tag : entry.replacement
|
|
437
|
+
return entry.replacement === undefined ? tag : replaceCanonicalPromptChips(entry.replacement, replacement => resolveChip(replacement.text));
|
|
446
438
|
});
|
|
447
439
|
return { text: expanded, images };
|
|
448
440
|
}
|
|
@@ -229,6 +229,8 @@ export declare class OwnedUiSessionShellRoot implements PiTuiComponentPort {
|
|
|
229
229
|
viewportTransientTailRowCount(): number;
|
|
230
230
|
hasActiveSelection(): boolean;
|
|
231
231
|
setViewportConfig(config: OwnedUiViewportSettings): void;
|
|
232
|
+
setFullscreenCopyOnSelect(enabled: boolean): void;
|
|
233
|
+
showCopyAcknowledgement(message: string): void;
|
|
232
234
|
resumeViewportFollowing(): void;
|
|
233
235
|
noteCompletedAssistantMessage(): void;
|
|
234
236
|
/** A new session binding owns new mounts even when it reuses semantic invocation ids. */
|
|
@@ -62,6 +62,8 @@ export class OwnedUiSessionShellRoot {
|
|
|
62
62
|
#lastWorkflowStatusId;
|
|
63
63
|
// Invariant: the notice is dock chrome, never transcript content; the custom viewport alone uses it.
|
|
64
64
|
#dockNotice;
|
|
65
|
+
#copyAcknowledgement;
|
|
66
|
+
#copyAcknowledgementTimer;
|
|
65
67
|
#inputSurface;
|
|
66
68
|
#inputSurfaceCoordination = "editor";
|
|
67
69
|
#dockInputReuseEnabled;
|
|
@@ -374,16 +376,20 @@ export class OwnedUiSessionShellRoot {
|
|
|
374
376
|
}
|
|
375
377
|
const height = Math.max(0, this.#componentRuntime.getRows());
|
|
376
378
|
const dock = this.#renderDockLayout(width);
|
|
377
|
-
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
const documentWidth =
|
|
379
|
+
const copyAcknowledgement = this.#renderCopyAcknowledgement(width);
|
|
380
|
+
// Invariant: auto/always own one stable final-column gutter. Every scrollable
|
|
381
|
+
// surface ends before it; the dock remains full-width and hidden mode returns it.
|
|
382
|
+
const documentWidth = this.#viewportController.config.scrollbarAppearance === "hidden"
|
|
383
|
+
? width
|
|
384
|
+
: Math.max(1, width - 1);
|
|
381
385
|
const document = this.#renderDocumentLayout(documentWidth);
|
|
382
|
-
const steeringRows = this.#renderQueued(
|
|
383
|
-
const statusRows = this.#renderStatus(
|
|
386
|
+
const steeringRows = this.#renderQueued(documentWidth);
|
|
387
|
+
const statusRows = this.#renderStatus(documentWidth);
|
|
384
388
|
const transientSignature = transientRowsSignature(steeringRows, statusRows);
|
|
385
389
|
const snapshot = this.#visibleViewportSnapshot;
|
|
386
|
-
|
|
390
|
+
// Invariant: paint-only feedback may cover either viewport or dock cells, so
|
|
391
|
+
// ordinary dock-only reuse cannot safely omit its overlay composition.
|
|
392
|
+
const dockInputCandidate = this.#dockInputCandidate && copyAcknowledgement === undefined;
|
|
387
393
|
// Concurrency: input may change after a keyboard receipt but before its paint.
|
|
388
394
|
// Capture only the live inputs this composition will actually represent.
|
|
389
395
|
const inputSurface = this.#inputSurface;
|
|
@@ -441,6 +447,12 @@ export class OwnedUiSessionShellRoot {
|
|
|
441
447
|
// Invariant: the control belongs immediately above the complete dock and
|
|
442
448
|
// floats over transient viewport content rather than consuming a dock row.
|
|
443
449
|
bottomControlRow: Math.max(0, height - Math.min(height, dockRows.length) - 1),
|
|
450
|
+
...(copyAcknowledgement === undefined || height <= 0 ? {} : {
|
|
451
|
+
frameOverlay: {
|
|
452
|
+
row: Math.max(1, Math.min(height, dockStartRow + dock.inputOffset - 1)),
|
|
453
|
+
text: copyAcknowledgement,
|
|
454
|
+
},
|
|
455
|
+
}),
|
|
444
456
|
theme: this.#viewportTheme,
|
|
445
457
|
});
|
|
446
458
|
this.#fullViewportCompositions += 1;
|
|
@@ -497,6 +509,25 @@ export class OwnedUiSessionShellRoot {
|
|
|
497
509
|
if (this.#viewportController.setConfig(config))
|
|
498
510
|
this.#documentLayouts.clear();
|
|
499
511
|
}
|
|
512
|
+
setFullscreenCopyOnSelect(enabled) {
|
|
513
|
+
this.#viewportController.setCopyOnSelect(enabled);
|
|
514
|
+
}
|
|
515
|
+
showCopyAcknowledgement(message) {
|
|
516
|
+
this.#clearCopyAcknowledgement(false);
|
|
517
|
+
this.#copyAcknowledgement = message;
|
|
518
|
+
const timer = setTimeout(() => {
|
|
519
|
+
if (this.#copyAcknowledgementTimer !== timer)
|
|
520
|
+
return;
|
|
521
|
+
this.#copyAcknowledgementTimer = undefined;
|
|
522
|
+
this.#copyAcknowledgement = undefined;
|
|
523
|
+
this.#invalidateChrome();
|
|
524
|
+
this.#componentRuntime.requestRender();
|
|
525
|
+
}, COPY_ACKNOWLEDGEMENT_DURATION_MS);
|
|
526
|
+
timer.unref?.();
|
|
527
|
+
this.#copyAcknowledgementTimer = timer;
|
|
528
|
+
this.#invalidateChrome();
|
|
529
|
+
this.#componentRuntime.requestRender();
|
|
530
|
+
}
|
|
500
531
|
resumeViewportFollowing() {
|
|
501
532
|
this.#viewportController.resumeFollowing();
|
|
502
533
|
}
|
|
@@ -543,12 +574,30 @@ export class OwnedUiSessionShellRoot {
|
|
|
543
574
|
const belowWidgets = this.#renderWidgets("belowEditor", width);
|
|
544
575
|
const footer = this.#renderFooter(width);
|
|
545
576
|
const rowsWithoutTransient = [...aboveWidgets, ...input, ...belowWidgets, ...footer];
|
|
577
|
+
const inputOffset = transientRows.length + aboveWidgets.length;
|
|
546
578
|
return {
|
|
547
579
|
rows: [...transientRows, ...rowsWithoutTransient],
|
|
548
|
-
|
|
580
|
+
inputOffset,
|
|
581
|
+
editorOffset: inputOffset + (body?.rowOffset ?? 0),
|
|
549
582
|
inputRows: body?.rowCount ?? input.length,
|
|
550
583
|
};
|
|
551
584
|
}
|
|
585
|
+
#renderCopyAcknowledgement(width) {
|
|
586
|
+
if (this.#copyAcknowledgement === undefined || width <= 0)
|
|
587
|
+
return undefined;
|
|
588
|
+
return piTheme().fg("accent", piShellTruncateToWidth(this.#copyAcknowledgement, width));
|
|
589
|
+
}
|
|
590
|
+
#clearCopyAcknowledgement(requestRender) {
|
|
591
|
+
if (this.#copyAcknowledgementTimer !== undefined)
|
|
592
|
+
clearTimeout(this.#copyAcknowledgementTimer);
|
|
593
|
+
this.#copyAcknowledgementTimer = undefined;
|
|
594
|
+
if (this.#copyAcknowledgement === undefined)
|
|
595
|
+
return;
|
|
596
|
+
this.#copyAcknowledgement = undefined;
|
|
597
|
+
this.#invalidateChrome();
|
|
598
|
+
if (requestRender)
|
|
599
|
+
this.#componentRuntime.requestRender();
|
|
600
|
+
}
|
|
552
601
|
#renderDockNotice(width) {
|
|
553
602
|
const notice = this.#dockNotice;
|
|
554
603
|
if (notice === undefined)
|
|
@@ -654,12 +703,9 @@ export class OwnedUiSessionShellRoot {
|
|
|
654
703
|
continue;
|
|
655
704
|
const promptLike = block !== undefined
|
|
656
705
|
&& (block.kind === "user" || this.#customViewport && isPiPromptStyleCompaction(block));
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
&& width > 1
|
|
661
|
-
? width - 1
|
|
662
|
-
: width;
|
|
706
|
+
// Invariant: the custom viewport has already removed its rail gutter from this width.
|
|
707
|
+
// Prompt-like and ordinary blocks therefore share one right content edge.
|
|
708
|
+
const blockWidth = width;
|
|
663
709
|
const blockRows = this.#blockRows(id, block, blockWidth);
|
|
664
710
|
if (promptLike) {
|
|
665
711
|
// Compatibility: the first natural prompt gets one breathing row at the document top.
|
|
@@ -930,6 +976,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
930
976
|
}
|
|
931
977
|
resetWorkflowPresentation() {
|
|
932
978
|
this.#dockNotice = undefined;
|
|
979
|
+
this.#clearCopyAcknowledgement(false);
|
|
933
980
|
for (const id of this.#workflowStatusAnchors.keys()) {
|
|
934
981
|
this.#transcript.get(id)?.dispose?.();
|
|
935
982
|
this.#transcript.delete(id);
|
|
@@ -1038,6 +1085,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
1038
1085
|
this.#inputSurface.setFocused?.(focused);
|
|
1039
1086
|
}
|
|
1040
1087
|
dispose() {
|
|
1088
|
+
this.#clearCopyAcknowledgement(false);
|
|
1041
1089
|
this.clearViewportPointerState();
|
|
1042
1090
|
this.#themeUnsubscribe();
|
|
1043
1091
|
this.header.dispose?.();
|
|
@@ -1189,6 +1237,7 @@ export class OwnedUiSessionShellRoot {
|
|
|
1189
1237
|
this.invalidate();
|
|
1190
1238
|
}
|
|
1191
1239
|
}
|
|
1240
|
+
const COPY_ACKNOWLEDGEMENT_DURATION_MS = 1_000;
|
|
1192
1241
|
const SCROLLBAR_CELL_RESET = "\u001b[22;23;24;25;27;28;29;39;54;55m";
|
|
1193
1242
|
const KITTY_IMAGE_CONTROL = /\u001b_G[\s\S]*?\u001b\\/g;
|
|
1194
1243
|
const ITERM_IMAGE_CONTROL = /\u001b]1337;File=[^\u0007]*(?:\u0007|\u001b\\)/g;
|
|
@@ -61,6 +61,7 @@ export class OwnedUiSessionShell {
|
|
|
61
61
|
#responseCopy;
|
|
62
62
|
// Rationale: only an executor this shell created owns a spare copy helper worth warming and disposing.
|
|
63
63
|
#copyExecutor;
|
|
64
|
+
#copyIntentSequence = 0;
|
|
64
65
|
#unbindClipboardWriter;
|
|
65
66
|
#damageTerminal;
|
|
66
67
|
#quitOutro;
|
|
@@ -235,6 +236,10 @@ export class OwnedUiSessionShell {
|
|
|
235
236
|
root: this.root,
|
|
236
237
|
mode: tuiMode,
|
|
237
238
|
...(this.#customViewport ? {
|
|
239
|
+
// Invariant: the owned shell enables and routes mouse reports itself. Pi's
|
|
240
|
+
// enclosing fullscreen renderer must never establish a competing white selection.
|
|
241
|
+
mouse: false,
|
|
242
|
+
consumeUnhandledMouse: true,
|
|
238
243
|
onOverlayGeometry: surfaces => this.root.setViewportOverlaySurfaces(surfaces),
|
|
239
244
|
decorateTerminal: (terminal) => {
|
|
240
245
|
damageTerminal = new DamageAwareTerminalAdapter(terminal, {
|
|
@@ -357,11 +362,15 @@ export class OwnedUiSessionShell {
|
|
|
357
362
|
const routed = this.root.handleViewportPreInput(data, true, Date.now(), this.root.usesDefaultInputSurface() && !this.runtime.hasFocusedOverlay());
|
|
358
363
|
if (routed.copySelection !== undefined) {
|
|
359
364
|
const snapshot = routed.copySelection;
|
|
365
|
+
const acknowledgement = copyAcknowledgement(snapshot);
|
|
366
|
+
const intent = ++this.#copyIntentSequence;
|
|
360
367
|
void this.#responseCopy?.submit(snapshot, pendingClipboardWrite).then(result => {
|
|
361
|
-
if (this.#disposed || !runtime.active
|
|
368
|
+
if (this.#disposed || !runtime.active || intent !== this.#copyIntentSequence
|
|
362
369
|
|| result.outcome !== "delivered" && result.outcome !== "submitted-unverified")
|
|
363
370
|
return;
|
|
364
|
-
|
|
371
|
+
if (acknowledgement.hasNonWhitespace) {
|
|
372
|
+
this.root.showCopyAcknowledgement(`copied ${acknowledgement.characters} chars to clipboard`);
|
|
373
|
+
}
|
|
365
374
|
});
|
|
366
375
|
}
|
|
367
376
|
if (!routed.consumed)
|
|
@@ -384,6 +393,7 @@ export class OwnedUiSessionShell {
|
|
|
384
393
|
this.root.setEditorPaddingX(initialPiSettings.editorPaddingX);
|
|
385
394
|
this.root.setAutocompleteMaxVisible(initialPiSettings.autocompleteMaxVisible);
|
|
386
395
|
this.root.setOutputPad(initialPiSettings.outputPad);
|
|
396
|
+
this.root.setFullscreenCopyOnSelect(initialPiSettings.fullscreenCopyOnSelect);
|
|
387
397
|
this.root.setHideThinkingBlock(initialPiSettings.hideThinkingBlock);
|
|
388
398
|
this.root.setMermaidRenderingMode(initialPiSettings.mermaidRenderingMode);
|
|
389
399
|
this.#showImages = initialPiSettings.showImages;
|
|
@@ -427,6 +437,11 @@ export class OwnedUiSessionShell {
|
|
|
427
437
|
this.#imageWidthCells = value;
|
|
428
438
|
this.root.setImagePresentation(this.#showImages, this.#imageWidthCells);
|
|
429
439
|
} },
|
|
440
|
+
...(this.#customViewport ? { fullscreenCopyOnSelect: { apply: (value) => {
|
|
441
|
+
if (typeof value !== "boolean")
|
|
442
|
+
throw new TypeError("Fullscreen copy-on-select setting is invalid");
|
|
443
|
+
this.root.setFullscreenCopyOnSelect(value);
|
|
444
|
+
} } } : {}),
|
|
430
445
|
});
|
|
431
446
|
if (this.#customViewport && promptHistory !== undefined) {
|
|
432
447
|
this.#promptHistoryImageSidecar = promptHistory.imageSidecar;
|
|
@@ -1522,6 +1537,7 @@ export class OwnedUiSessionShell {
|
|
|
1522
1537
|
}
|
|
1523
1538
|
async #dispose() {
|
|
1524
1539
|
this.#disposed = true;
|
|
1540
|
+
this.#copyIntentSequence += 1;
|
|
1525
1541
|
// Invariant: the outro frame is what the terminal shows now, before any cleanup writes.
|
|
1526
1542
|
const outroFrame = this.#captureQuitOutroFrame();
|
|
1527
1543
|
this.#responseCopy?.dispose();
|
|
@@ -1662,6 +1678,7 @@ export class OwnedUiSessionShell {
|
|
|
1662
1678
|
this.#streamPresentation.noteImmediatePresentation();
|
|
1663
1679
|
const view = this.view();
|
|
1664
1680
|
if (this.backend.sessionGeneration !== this.#sessionGeneration) {
|
|
1681
|
+
this.#copyIntentSequence += 1;
|
|
1665
1682
|
this.#responseCopy?.reset();
|
|
1666
1683
|
this.#cancelWaitingImages();
|
|
1667
1684
|
this.root.resetPendingPastes();
|
|
@@ -2078,6 +2095,17 @@ export class OwnedUiSessionShell {
|
|
|
2078
2095
|
function isCoalescedStreamBlock(kind) {
|
|
2079
2096
|
return kind === "assistant" || kind === "thinking" || kind === "tool-call" || kind === "tool-result";
|
|
2080
2097
|
}
|
|
2098
|
+
function copyAcknowledgement(snapshot) {
|
|
2099
|
+
if (snapshot.literal !== true)
|
|
2100
|
+
return { characters: snapshot.sourceUnits, hasNonWhitespace: true };
|
|
2101
|
+
let characters = Math.max(0, snapshot.rows.length - 1);
|
|
2102
|
+
let hasNonWhitespace = false;
|
|
2103
|
+
for (const row of snapshot.rows) {
|
|
2104
|
+
characters += row.text.length;
|
|
2105
|
+
hasNonWhitespace ||= /\S/u.test(row.text);
|
|
2106
|
+
}
|
|
2107
|
+
return { characters, hasNonWhitespace };
|
|
2108
|
+
}
|
|
2081
2109
|
export function formatSessionResumeCommand(metadata) {
|
|
2082
2110
|
const args = [PRODUCT_TEXT.commandName];
|
|
2083
2111
|
if (!metadata.usesDefaultSessionDir)
|
|
@@ -39,6 +39,7 @@ export declare class SessionViewportController {
|
|
|
39
39
|
readonly column: number;
|
|
40
40
|
readonly row: number;
|
|
41
41
|
} | undefined;
|
|
42
|
+
setCopyOnSelect(enabled: boolean): void;
|
|
42
43
|
get frame(): TranscriptViewportFrame | null;
|
|
43
44
|
setEditorPointerFrame(frame: {
|
|
44
45
|
readonly rowStart: number;
|
|
@@ -44,6 +44,7 @@ export class SessionViewportController {
|
|
|
44
44
|
#hoveredHyperlinkKey;
|
|
45
45
|
#lastRequestedSelectionRevision = -1;
|
|
46
46
|
#presentationRevision = 0;
|
|
47
|
+
#copyOnSelect = true;
|
|
47
48
|
#activityTimer;
|
|
48
49
|
constructor(options) {
|
|
49
50
|
this.#enabled = options.enabled;
|
|
@@ -79,6 +80,9 @@ export class SessionViewportController {
|
|
|
79
80
|
get pointerPosition() {
|
|
80
81
|
return this.#pointerPosition;
|
|
81
82
|
}
|
|
83
|
+
setCopyOnSelect(enabled) {
|
|
84
|
+
this.#copyOnSelect = enabled;
|
|
85
|
+
}
|
|
82
86
|
get frame() {
|
|
83
87
|
return this.#viewport.frame;
|
|
84
88
|
}
|
|
@@ -504,7 +508,7 @@ export class SessionViewportController {
|
|
|
504
508
|
this.#editor.handlePointer({ kind: "press", button: 0, column: pendingEditorClick.column, row: editorRow });
|
|
505
509
|
this.#editor.handlePointer({ kind: "release", button: 0, column: pendingEditorClick.column, row: editorRow });
|
|
506
510
|
}
|
|
507
|
-
else {
|
|
511
|
+
else if (this.#copyOnSelect) {
|
|
508
512
|
completedCopy = this.#viewport.captureSelectedText() ?? undefined;
|
|
509
513
|
}
|
|
510
514
|
// Platform: restore OSC 8 links only after the held-button selection paint has
|
|
@@ -5,6 +5,8 @@ export type { OwnedUiBlockStatus, OwnedUiCommand, OwnedUiCommandOutcome, OwnedUi
|
|
|
5
5
|
export { acceptsTranscriptUpdate, transcriptToolState } from "./transcript-lifecycle.js";
|
|
6
6
|
export { ImageAttachmentError, MAX_CLIPBOARD_TEXT_BYTES, MAX_IMAGE_DATA_BYTES, MAX_PROMPT_IMAGES, assertImageEncodedSize, assertPromptImages, } from "./image-attachments.js";
|
|
7
7
|
export { CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, normalizePromptSuggestionCandidate } from "./prompt-suggestions.js";
|
|
8
|
+
export { canonicalPromptChipMatches, protectPromptChipWrapping, replaceCanonicalPromptChips } from "./prompt-chips.js";
|
|
9
|
+
export type { PromptChipTextMatch, PromptChipWrapProtection } from "./prompt-chips.js";
|
|
8
10
|
export { SUGGESTION_DECISION_REASONS, SUGGESTION_DIAGNOSTIC_EVENTS } from "./suggestion-diagnostics.js";
|
|
9
11
|
export type { SuggestionDecision, SuggestionDecisionReason, SuggestionDiagnosticEvent, SuggestionDiagnosticObserver, SuggestionDiagnosticRecord, } from "./suggestion-diagnostics.js";
|
|
10
12
|
export { PROMPT_HISTORY_EDITOR_REPLACEMENT, PROMPT_HISTORY_MAX_ENTRY_BYTES, PROMPT_HISTORY_MAX_PENDING, PROMPT_HISTORY_MAX_TEXT_BYTES, assertPromptHistorySubmission, } from "./prompt-history.js";
|
|
@@ -3,6 +3,7 @@ export { OWNED_UI_CONTRACT_VERSION } from "./model.js";
|
|
|
3
3
|
export { acceptsTranscriptUpdate, transcriptToolState } from "./transcript-lifecycle.js";
|
|
4
4
|
export { ImageAttachmentError, MAX_CLIPBOARD_TEXT_BYTES, MAX_IMAGE_DATA_BYTES, MAX_PROMPT_IMAGES, assertImageEncodedSize, assertPromptImages, } from "./image-attachments.js";
|
|
5
5
|
export { CONTEXTUAL_PROMPT_SUGGESTION_INSTRUCTION, normalizePromptSuggestionCandidate } from "./prompt-suggestions.js";
|
|
6
|
+
export { canonicalPromptChipMatches, protectPromptChipWrapping, replaceCanonicalPromptChips } from "./prompt-chips.js";
|
|
6
7
|
export { SUGGESTION_DECISION_REASONS, SUGGESTION_DIAGNOSTIC_EVENTS } from "./suggestion-diagnostics.js";
|
|
7
8
|
export { PROMPT_HISTORY_EDITOR_REPLACEMENT, PROMPT_HISTORY_MAX_ENTRY_BYTES, PROMPT_HISTORY_MAX_PENDING, PROMPT_HISTORY_MAX_TEXT_BYTES, assertPromptHistorySubmission, } from "./prompt-history.js";
|
|
8
9
|
export { assertOwnedUiCommand, assertOwnedUiCustomization, assertOwnedUiDiagnostics, assertOwnedUiEditorState, assertOwnedUiEvent, assertOwnedUiPromptSuggestionIdentity, assertOwnedUiPromptSuggestionRequest, assertOwnedUiPromptSuggestionResult, assertOwnedUiPromptSuggestionState, assertOwnedUiSessionViewModel, assertOwnedUiSnapshot, assertOwnedUiStatusView, assertOwnedUiTerminalSurface, assertOwnedUiTranscriptBlock, } from "./validation.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface PromptChipTextMatch {
|
|
2
|
+
readonly text: string;
|
|
3
|
+
readonly start: number;
|
|
4
|
+
readonly end: number;
|
|
5
|
+
}
|
|
6
|
+
export interface PromptChipWrapProtection {
|
|
7
|
+
readonly text: string;
|
|
8
|
+
restore(rendered: string): string;
|
|
9
|
+
}
|
|
10
|
+
/** Finds canonical prompt-chip labels without requiring their process-local backing values. */
|
|
11
|
+
export declare function canonicalPromptChipMatches(text: string): readonly PromptChipTextMatch[];
|
|
12
|
+
/** Replaces only canonical prompt-chip labels while preserving every intervening source byte. */
|
|
13
|
+
export declare function replaceCanonicalPromptChips(text: string, replacement: (match: PromptChipTextMatch) => string): string;
|
|
14
|
+
/** Protects fitting chip labels for Markdown wrapping and restores their exact visible source text. */
|
|
15
|
+
export declare function protectPromptChipWrapping(text: string): PromptChipWrapProtection;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const PROMPT_CHIP_PATTERN = /\[(?:paste #\d+ (?:\+\d+ lines|\d+ chars)|📷 [^\]]+|📁 [^\]]+|📄 [^\]]+|🖼 {1,2}[^\]]+|🔗 [^\]]+)\]/gu;
|
|
2
|
+
const ZERO_WIDTH_MARKERS = ["\u2060", "\u200b", "\u200c", "\u200d", "\ufeff"];
|
|
3
|
+
/** Finds canonical prompt-chip labels without requiring their process-local backing values. */
|
|
4
|
+
export function canonicalPromptChipMatches(text) {
|
|
5
|
+
return [...text.matchAll(PROMPT_CHIP_PATTERN)].map(match => ({
|
|
6
|
+
text: match[0], start: match.index, end: match.index + match[0].length,
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
9
|
+
/** Replaces only canonical prompt-chip labels while preserving every intervening source byte. */
|
|
10
|
+
export function replaceCanonicalPromptChips(text, replacement) {
|
|
11
|
+
return text.replace(PROMPT_CHIP_PATTERN, (value, offset) => replacement({
|
|
12
|
+
text: value, start: offset, end: offset + value.length,
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
/** Protects fitting chip labels for Markdown wrapping and restores their exact visible source text. */
|
|
16
|
+
export function protectPromptChipWrapping(text) {
|
|
17
|
+
const matches = canonicalPromptChipMatches(text);
|
|
18
|
+
if (matches.length === 0)
|
|
19
|
+
return { text, restore: identity };
|
|
20
|
+
const spaceMarker = unusedPrivateMarker(text);
|
|
21
|
+
const boundaryMarker = unusedZeroWidthMarker(text);
|
|
22
|
+
let protectedText = "", cursor = 0;
|
|
23
|
+
for (const match of matches) {
|
|
24
|
+
protectedText += text.slice(cursor, match.start);
|
|
25
|
+
// Rationale: adjacent chips need a temporary break opportunity or Markdown treats the complete run as one word.
|
|
26
|
+
if (match.start === cursor && cursor > 0)
|
|
27
|
+
protectedText += `${boundaryMarker} `;
|
|
28
|
+
protectedText += match.text.replaceAll(" ", spaceMarker);
|
|
29
|
+
cursor = match.end;
|
|
30
|
+
}
|
|
31
|
+
protectedText += text.slice(cursor);
|
|
32
|
+
return {
|
|
33
|
+
text: protectedText,
|
|
34
|
+
restore: rendered => rendered
|
|
35
|
+
.replaceAll(`${boundaryMarker} `, "")
|
|
36
|
+
.replaceAll(boundaryMarker, "")
|
|
37
|
+
.replaceAll(spaceMarker, " "),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function unusedPrivateMarker(text) {
|
|
41
|
+
for (let codePoint = 0xe000; codePoint <= 0xf8ff; codePoint++) {
|
|
42
|
+
const marker = String.fromCodePoint(codePoint);
|
|
43
|
+
if (!text.includes(marker))
|
|
44
|
+
return marker;
|
|
45
|
+
}
|
|
46
|
+
throw new Error("Prompt chip text exhausts the private presentation marker range");
|
|
47
|
+
}
|
|
48
|
+
function unusedZeroWidthMarker(text) {
|
|
49
|
+
for (const marker of ZERO_WIDTH_MARKERS)
|
|
50
|
+
if (!text.includes(marker))
|
|
51
|
+
return marker;
|
|
52
|
+
for (const left of ZERO_WIDTH_MARKERS) {
|
|
53
|
+
for (const right of ZERO_WIDTH_MARKERS)
|
|
54
|
+
if (!text.includes(left + right))
|
|
55
|
+
return left + right;
|
|
56
|
+
}
|
|
57
|
+
throw new Error("Prompt chip text exhausts the zero-width presentation marker range");
|
|
58
|
+
}
|
|
59
|
+
function identity(value) { return value; }
|
|
@@ -353,7 +353,7 @@ export function assertOwnedUiStatusView(status) {
|
|
|
353
353
|
assertBoundedText(status.title, "owned-UI status title", MAX_LABEL_LENGTH);
|
|
354
354
|
assertOptionalText(status.workingMessage, "owned-UI working message", MAX_MESSAGE_LENGTH);
|
|
355
355
|
if (status.workingProgress !== undefined && status.workingProgress !== null) {
|
|
356
|
-
assertIntegerInRange(status.workingProgress, 0,
|
|
356
|
+
assertIntegerInRange(status.workingProgress, 0, 100, "owned-UI working progress");
|
|
357
357
|
}
|
|
358
358
|
assertCollection(status.diagnostics, "owned-UI status diagnostics", MAX_STATUS_DIAGNOSTICS);
|
|
359
359
|
for (const diagnostic of status.diagnostics) {
|
|
@@ -10,6 +10,7 @@ export { PINNED_PI_LAYOUT, applyConfiguredPiTheme, applyPiTheme, applyPiThemeIns
|
|
|
10
10
|
export type { PiColorMode, PiTerminalTheme, PiTerminalThemeDetection, PiTerminalThemeDetector, PiThemeBackground, PiThemeResult } from "./theme.js";
|
|
11
11
|
export { OwnedPiThemeController } from "./upstream/theme/theme-controller.js";
|
|
12
12
|
export type { PiThemeRuntimePort, PiThemeSettingsPort } from "./upstream/theme/theme-controller.js";
|
|
13
|
+
export { PI_MODAL_CONTENT_PADDING_X, PiModalFrame, PiModalHeader, addPiModalHeader, adoptPiModalFrame, adoptPiModalHeader, } from "./modal-frame.js";
|
|
13
14
|
export { ModelsDialogComponent } from "./models-dialog.js";
|
|
14
15
|
export type { ModelsDialogCallbacks, ModelsDialogConfig, ModelsDialogFilter, ModelsDialogModel } from "./models-dialog.js";
|
|
15
16
|
export { CountdownTimer } from "./upstream/components/countdown-timer.js";
|
|
@@ -5,6 +5,7 @@ export { OWNED_BUILTIN_SLASH_COMMANDS, PINNED_PI_BUILTIN_SLASH_COMMANDS, compone
|
|
|
5
5
|
export { renderPiShellHotkeySections } from "./shell-hotkey-sections.js";
|
|
6
6
|
export { PINNED_PI_LAYOUT, applyConfiguredPiTheme, applyPiTheme, applyPiThemeInstance, currentPiThemeName, detectPiTerminalBackgroundFromEnv, detectPiTerminalBackgroundTheme, detectPiTerminalThemeForAuto, ensurePiTheme, getAvailablePiThemes, loadPiTheme, onPiThemeChange, parsePiAutoThemeSetting, piTheme, resolvePiThemeSetting, stopPiThemeWatcher, } from "./theme.js";
|
|
7
7
|
export { OwnedPiThemeController } from "./upstream/theme/theme-controller.js";
|
|
8
|
+
export { PI_MODAL_CONTENT_PADDING_X, PiModalFrame, PiModalHeader, addPiModalHeader, adoptPiModalFrame, adoptPiModalHeader, } from "./modal-frame.js";
|
|
8
9
|
export { ModelsDialogComponent } from "./models-dialog.js";
|
|
9
10
|
export { CountdownTimer } from "./upstream/components/countdown-timer.js";
|
|
10
11
|
export { ExtensionEditorComponent } from "./upstream/components/extension-editor.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Container, type Component } from "@earendil-works/pi-tui";
|
|
2
|
+
export declare const PI_MODAL_CONTENT_PADDING_X = 1;
|
|
3
|
+
/** The semantic top rule/title pair used while a modal assembles its frame. */
|
|
4
|
+
export declare class PiModalHeader implements Component {
|
|
5
|
+
readonly rule: Component;
|
|
6
|
+
readonly title: Component;
|
|
7
|
+
constructor(rule: Component, title: Component);
|
|
8
|
+
invalidate(): void;
|
|
9
|
+
render(width: number): string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Complete framed-modal chrome. Rules remain frame-wide; every semantic content component renders
|
|
13
|
+
* in the same one-cell-smaller rectangle and receives the same left inset.
|
|
14
|
+
*/
|
|
15
|
+
export declare class PiModalFrame implements Component {
|
|
16
|
+
#private;
|
|
17
|
+
constructor(topRule: Component, content: readonly Component[], bottomRule: Component, fullWidthContent?: readonly Component[], preInsetContent?: readonly Component[]);
|
|
18
|
+
invalidate(): void;
|
|
19
|
+
render(width: number): string[];
|
|
20
|
+
}
|
|
21
|
+
/** Add compact titled-modal chrome while a component assembles its semantic body. */
|
|
22
|
+
export declare function addPiModalHeader(container: Container, rule: Component, title: Component): PiModalHeader;
|
|
23
|
+
export interface PiModalFrameAdoption {
|
|
24
|
+
readonly topIndex: number;
|
|
25
|
+
readonly bottomIndex: number;
|
|
26
|
+
readonly header?: PiModalHeader;
|
|
27
|
+
readonly fullWidthContent?: readonly Component[];
|
|
28
|
+
/** Public pinned children that already own the same one-cell outer inset. */
|
|
29
|
+
readonly preInsetContent?: readonly Component[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Replace explicitly identified structural modal children with the shared padded frame. Callers
|
|
33
|
+
* identify semantic rules/components; rendered text is never inspected or rewritten.
|
|
34
|
+
*/
|
|
35
|
+
export declare function adoptPiModalFrame(container: Container, adoption: PiModalFrameAdoption): PiModalFrame;
|
|
36
|
+
/**
|
|
37
|
+
* Adopt a public component's semantic rule/title children at the bare-A1 boundary. Only an optional
|
|
38
|
+
* structural Spacer between those explicitly identified children is accepted; rendered text is
|
|
39
|
+
* never inspected or rewritten. The pinned comparison profile does not call this adapter.
|
|
40
|
+
*/
|
|
41
|
+
export declare function adoptPiModalHeader(container: Container, ruleIndex: number, titleIndex: number): PiModalHeader;
|