@timurproko/a1 0.1.8-dev.136 → 0.1.8-dev.139

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.
Files changed (81) hide show
  1. package/README.md +2 -4
  2. package/dist/cli/capabilities.d.ts +2 -11
  3. package/dist/cli/capabilities.js +1 -1
  4. package/dist/cli/dispatch.js +3 -6
  5. package/dist/composition/owned-ui.js +19 -1
  6. package/dist/contracts/owned-ui/model.d.ts +21 -0
  7. package/dist/contracts/owned-ui/validation.js +3 -0
  8. package/dist/features/launch/development-launch.js +11 -3
  9. package/dist/features/launch/index.d.ts +0 -1
  10. package/dist/features/launch/index.js +0 -1
  11. package/dist/features/launch/intent.d.ts +3 -2
  12. package/dist/features/launch/intent.js +1 -2
  13. package/dist/features/launch/prepare-launch.d.ts +0 -2
  14. package/dist/features/launch/prepare-launch.js +2 -9
  15. package/dist/features/launch/profile-paths.d.ts +1 -2
  16. package/dist/features/launch/profile-paths.js +2 -8
  17. package/dist/features/launch/runtime-selection.d.ts +2 -5
  18. package/dist/features/owned-ui/settings-app.js +36 -13
  19. package/dist/foundation/lifecycle/model.d.ts +1 -1
  20. package/dist/foundation/lifecycle/model.js +1 -1
  21. package/dist/foundation/release/bootstrap.d.ts +1 -3
  22. package/dist/foundation/release/bootstrap.js +12 -4
  23. package/dist/foundation/storage/control-store.js +37 -3
  24. package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
  25. package/dist/integrations/pi/components/shell-footer-status.js +14 -5
  26. package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -2
  27. package/dist/integrations/pi/components/shell-presenters-transcript.js +9 -8
  28. package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +13 -0
  29. package/dist/integrations/pi/components/submitted-prompt-adapter.js +27 -0
  30. package/dist/integrations/pi/engine/adapter.js +16 -2
  31. package/dist/integrations/pi/engine/session-integration.js +5 -2
  32. package/dist/integrations/pi/owned-ui/session-shell-root.d.ts +17 -2
  33. package/dist/integrations/pi/owned-ui/session-shell-root.js +451 -42
  34. package/dist/integrations/pi/owned-ui/session-shell.js +116 -13
  35. package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -1
  36. package/dist/integrations/pi/tui-runtime/adapter.js +51 -2
  37. package/dist/integrations/pi/tui-runtime/contracts.d.ts +2 -0
  38. package/dist/native/darwin-arm64/manifest.json +1 -1
  39. package/dist/native/linux-x64/manifest.json +1 -1
  40. package/dist/native/win32-x64/manifest.json +2 -2
  41. package/dist/native/win32-x64/process-guardian.exe +0 -0
  42. package/dist/product-identity.d.ts +2 -2
  43. package/dist/product-identity.js +3 -4
  44. package/dist/product-identity.json +3 -5
  45. package/dist/ui/components/index.d.ts +3 -0
  46. package/dist/ui/components/index.js +3 -0
  47. package/dist/ui/components/mouse.d.ts +10 -0
  48. package/dist/ui/components/mouse.js +23 -0
  49. package/dist/ui/components/prompt-row.d.ts +14 -0
  50. package/dist/ui/components/prompt-row.js +20 -0
  51. package/dist/ui/components/scrollbar.d.ts +26 -0
  52. package/dist/ui/components/scrollbar.js +35 -0
  53. package/dist/ui/components/spans.d.ts +6 -0
  54. package/dist/ui/components/spans.js +36 -0
  55. package/dist/ui/components/submitted-prompt.d.ts +23 -0
  56. package/dist/ui/components/submitted-prompt.js +50 -0
  57. package/dist/ui/components/text-selection.d.ts +53 -0
  58. package/dist/ui/components/text-selection.js +168 -0
  59. package/dist/ui/components/text.d.ts +14 -0
  60. package/dist/ui/components/text.js +40 -0
  61. package/dist/ui/components/transcript-viewport.d.ts +98 -0
  62. package/dist/ui/components/transcript-viewport.js +353 -0
  63. package/dist/ui/settings/declarations.d.ts +8 -1
  64. package/dist/ui/settings/declarations.js +33 -5
  65. package/dist/ui/settings/migrations.d.ts +1 -1
  66. package/dist/ui/settings/migrations.js +12 -2
  67. package/dist/ui/settings/sections.js +15 -8
  68. package/dist/ui/settings/session.d.ts +2 -0
  69. package/dist/ui/settings/session.js +7 -5
  70. package/docs/architecture/boundaries.md +2 -2
  71. package/docs/architecture/project-structure.md +1 -1
  72. package/docs/architecture/resource-and-data-policy.md +1 -1
  73. package/docs/architecture/toolchain.md +5 -5
  74. package/docs/architecture/ui-reference-provenance.md +11 -2
  75. package/docs/features/launch-profiles.md +7 -14
  76. package/docs/manual-launch-instance-acceptance.md +12 -35
  77. package/docs/manual-owned-ui-checkpoint.md +2 -3
  78. package/docs/manual-pi-boundary-candidate.md +4 -5
  79. package/package.json +1 -2
  80. package/dist/features/launch/profiles.d.ts +0 -12
  81. package/dist/features/launch/profiles.js +0 -26
@@ -19,6 +19,30 @@ export interface ScrollbarInput {
19
19
  readonly scroll: number;
20
20
  readonly trackHeight: number;
21
21
  }
22
+ export type ScrollbarAppearance = "always" | "hover" | "hidden";
23
+ export type ScrollbarStyle = "thin" | "thick";
24
+ export type ScrollbarSpeed = "normal" | "fast" | "high";
25
+ /** Wheel distance: normal (3), fast (6), high as normal + fast (9). */
26
+ export declare function scrollbarWheelRows(speed: ScrollbarSpeed): number;
27
+ /** Selection edge distance at each fixed 30ms tick. */
28
+ export declare function scrollbarSelectionRows(speed: ScrollbarSpeed): number;
29
+ export interface ScrollbarPresentationInput {
30
+ readonly geometry: ScrollbarGeometry | null;
31
+ readonly appearance: ScrollbarAppearance;
32
+ readonly style: ScrollbarStyle;
33
+ readonly hovered: boolean;
34
+ readonly dragging: boolean;
35
+ readonly activeUntil: number;
36
+ readonly now: number;
37
+ }
38
+ export interface ScrollbarPresentation {
39
+ readonly visible: boolean;
40
+ readonly reservesSpace: boolean;
41
+ readonly trackGlyph: "│" | "┃";
42
+ readonly thumbGlyph: "│" | "┃";
43
+ }
44
+ /** Pure appearance decision; activity time is supplied so tests own the clock. */
45
+ export declare function scrollbarPresentation(input: ScrollbarPresentationInput): ScrollbarPresentation;
22
46
  /** Null when the content fits: no scrollbar is drawn and no width is reserved. */
23
47
  export declare function scrollbarGeometry(input: ScrollbarInput): ScrollbarGeometry | null;
24
48
  export declare function scrollbarReservesSpace(geometry: ScrollbarGeometry | null): boolean;
@@ -48,6 +72,8 @@ export interface RailPointer {
48
72
  */
49
73
  export declare class ScrollbarRails {
50
74
  #private;
75
+ noteActivity(key: string, now: number, lingerMs?: number): void;
76
+ isRecentlyActive(key: string, now: number): boolean;
51
77
  isHovered(key: string): boolean;
52
78
  isDragging(key: string): boolean;
53
79
  get draggingKey(): string | null;
@@ -3,6 +3,33 @@
3
3
  * rail rather than to a global, because two scrollable surfaces can be visible
4
4
  * at once and a shared flag lights up the wrong one.
5
5
  */
6
+ /** Wheel distance: normal (3), fast (6), high as normal + fast (9). */
7
+ export function scrollbarWheelRows(speed) {
8
+ return speed === "high" ? 9 : speed === "fast" ? 6 : 3;
9
+ }
10
+ /** Selection edge distance at each fixed 30ms tick. */
11
+ export function scrollbarSelectionRows(speed) {
12
+ return speed === "high" ? 3 : speed === "fast" ? 2 : 1;
13
+ }
14
+ /** Pure appearance decision; activity time is supplied so tests own the clock. */
15
+ export function scrollbarPresentation(input) {
16
+ const overflowing = input.geometry !== null;
17
+ // As in v2, always/hover reserve the final rail cell even while content fits.
18
+ // The blank gutter prevents prompt timestamps from touching the terminal edge
19
+ // and keeps later scrollbar appearance from reflowing the transcript.
20
+ const reservesSpace = input.appearance !== "hidden";
21
+ const visible = overflowing && reservesSpace && (input.appearance === "always"
22
+ || input.hovered
23
+ || input.dragging
24
+ || (input.activeUntil > 0 && input.now <= input.activeUntil));
25
+ const thick = input.style === "thick";
26
+ return {
27
+ visible,
28
+ reservesSpace,
29
+ trackGlyph: thick ? "┃" : "│",
30
+ thumbGlyph: thick || input.hovered || input.dragging ? "┃" : "│",
31
+ };
32
+ }
6
33
  /** Null when the content fits: no scrollbar is drawn and no width is reserved. */
7
34
  export function scrollbarGeometry(input) {
8
35
  const { contentLength, viewportHeight, trackHeight } = input;
@@ -51,7 +78,14 @@ export function scrollForTrackPage(geometry, trackRow, scroll, viewportHeight) {
51
78
  */
52
79
  export class ScrollbarRails {
53
80
  #hovered = new Set();
81
+ #activeUntil = new Map();
54
82
  #dragging;
83
+ noteActivity(key, now, lingerMs = 900) {
84
+ this.#activeUntil.set(key, Math.max(this.#activeUntil.get(key) ?? 0, now + Math.max(0, lingerMs)));
85
+ }
86
+ isRecentlyActive(key, now) {
87
+ return now <= (this.#activeUntil.get(key) ?? 0);
88
+ }
55
89
  isHovered(key) {
56
90
  return this.#hovered.has(key);
57
91
  }
@@ -97,6 +131,7 @@ export class ScrollbarRails {
97
131
  }
98
132
  clear() {
99
133
  this.#hovered.clear();
134
+ this.#activeUntil.clear();
100
135
  this.#dragging = undefined;
101
136
  }
102
137
  }
@@ -5,3 +5,9 @@
5
5
  * replaced by spaces rather than split.
6
6
  */
7
7
  export declare function overlaySpan(line: string, from: number, to: number, span: string): string;
8
+ /**
9
+ * Paints only a background over [from,to), preserving every glyph and its
10
+ * foreground, bold, underline, and hyperlink styling. Embedded resets reapply
11
+ * the selection background, while leaving the span restores the row's styles.
12
+ */
13
+ export declare function backgroundSgrSpan(line: string, from: number, to: number, on?: string, off?: string): string;
@@ -9,6 +9,7 @@ const ANSI_SEQUENCE = "\\u001b(?:\\[[0-?]*[ -/]*[@-~]|[\\]_][^\\u0007\\u001b]*(?
9
9
  const ANSI_SPLIT = new RegExp(`(${ANSI_SEQUENCE})`);
10
10
  const HYPERLINK = /^\]8;;([^]*)(?:|\\)?$/;
11
11
  const HYPERLINK_OFF = "]8;;\\";
12
+ const SGR = /^\[[0-9;]*m$/;
12
13
  const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: "grapheme" });
13
14
  /**
14
15
  * Replaces the visible columns `[from, to)` of `line` with `span`, preserving
@@ -57,3 +58,38 @@ export function overlaySpan(line, from, to, span) {
57
58
  head += " ".repeat(from - column);
58
59
  return head + (hyperlinkOpen ? HYPERLINK_OFF : "") + span + replay + tail;
59
60
  }
61
+ /**
62
+ * Paints only a background over [from,to), preserving every glyph and its
63
+ * foreground, bold, underline, and hyperlink styling. Embedded resets reapply
64
+ * the selection background, while leaving the span restores the row's styles.
65
+ */
66
+ export function backgroundSgrSpan(line, from, to, on = "\u001b[47m", off = "\u001b[49m") {
67
+ let output = "";
68
+ let seen = "";
69
+ let column = 0;
70
+ let inside = false;
71
+ for (const token of line.split(ANSI_SPLIT)) {
72
+ if (!token)
73
+ continue;
74
+ if (token.startsWith("\u001b")) {
75
+ output += token;
76
+ if (SGR.test(token))
77
+ seen += token;
78
+ if (inside)
79
+ output += on;
80
+ continue;
81
+ }
82
+ for (const { segment } of GRAPHEMES.segment(token)) {
83
+ const shouldBeInside = column >= from && column < to;
84
+ if (shouldBeInside !== inside) {
85
+ output += shouldBeInside ? on : off + seen;
86
+ inside = shouldBeInside;
87
+ }
88
+ output += segment;
89
+ column += displayWidth(segment);
90
+ }
91
+ }
92
+ if (inside)
93
+ output += off;
94
+ return output;
95
+ }
@@ -0,0 +1,23 @@
1
+ /** Visible width of the submitted-prompt prefix (`❯ `). */
2
+ export declare const SUBMITTED_PROMPT_PREFIX_WIDTH = 2;
3
+ export interface SubmittedPromptStyle {
4
+ readonly prefix?: (text: string) => string;
5
+ readonly timestamp?: (text: string) => string;
6
+ }
7
+ export interface SubmittedPromptLayout {
8
+ readonly contentWidth: number;
9
+ readonly timestamp: string | null;
10
+ }
11
+ /** Formats transcript metadata, never render time, as local 24-hour HH:mm. */
12
+ export declare function formatSubmittedPromptTime(source: Date | number): string | null;
13
+ /**
14
+ * Computes the width at which the prompt's Markdown body must be rendered.
15
+ * The timestamp is omitted rather than allowing it to crowd useful content.
16
+ */
17
+ export declare function submittedPromptLayout(width: number, source?: Date | number | null): SubmittedPromptLayout;
18
+ /**
19
+ * Composes already-rendered prompt content into the compact submitted bar.
20
+ * Continuations align below the content, not below the prefix, and no row can
21
+ * exceed the supplied terminal width even when the content renderer misbehaves.
22
+ */
23
+ export declare function composeSubmittedPromptRows(contentRows: readonly string[], width: number, source?: Date | number | null, style?: SubmittedPromptStyle): readonly string[];
@@ -0,0 +1,50 @@
1
+ import { displayWidth, truncateToWidth } from "./text.js";
2
+ /** Visible width of the submitted-prompt prefix (`❯ `). */
3
+ export const SUBMITTED_PROMPT_PREFIX_WIDTH = 2;
4
+ const TIMESTAMP_MARGIN = 3;
5
+ const MIN_USEFUL_CONTENT_WIDTH = 8;
6
+ /** Formats transcript metadata, never render time, as local 24-hour HH:mm. */
7
+ export function formatSubmittedPromptTime(source) {
8
+ const date = source instanceof Date ? source : new Date(source);
9
+ if (!Number.isFinite(date.getTime()))
10
+ return null;
11
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
12
+ }
13
+ /**
14
+ * Computes the width at which the prompt's Markdown body must be rendered.
15
+ * The timestamp is omitted rather than allowing it to crowd useful content.
16
+ */
17
+ export function submittedPromptLayout(width, source) {
18
+ const boundedWidth = Math.max(1, width);
19
+ const timestamp = source === undefined || source === null ? null : formatSubmittedPromptTime(source);
20
+ const timestampReserve = timestamp === null ? 0 : displayWidth(timestamp) + TIMESTAMP_MARGIN;
21
+ const canShowTimestamp = timestamp !== null
22
+ && boundedWidth - SUBMITTED_PROMPT_PREFIX_WIDTH - timestampReserve >= MIN_USEFUL_CONTENT_WIDTH;
23
+ return {
24
+ contentWidth: Math.max(1, boundedWidth - SUBMITTED_PROMPT_PREFIX_WIDTH - (canShowTimestamp ? timestampReserve : 0)),
25
+ timestamp: canShowTimestamp ? timestamp : null,
26
+ };
27
+ }
28
+ /**
29
+ * Composes already-rendered prompt content into the compact submitted bar.
30
+ * Continuations align below the content, not below the prefix, and no row can
31
+ * exceed the supplied terminal width even when the content renderer misbehaves.
32
+ */
33
+ export function composeSubmittedPromptRows(contentRows, width, source, style = {}) {
34
+ if (width <= 0)
35
+ return [];
36
+ const layout = submittedPromptLayout(width, source);
37
+ const prefix = (style.prefix ?? identity)("❯ ");
38
+ const rows = (contentRows.length === 0 ? [""] : contentRows).map((row, index) => {
39
+ const content = truncateToWidth(row, layout.contentWidth);
40
+ return `${index === 0 ? prefix : " ".repeat(SUBMITTED_PROMPT_PREFIX_WIDTH)}${content}`;
41
+ });
42
+ if (layout.timestamp !== null && rows[0] !== undefined) {
43
+ const gap = Math.max(1, width - displayWidth(rows[0]) - displayWidth(layout.timestamp));
44
+ rows[0] = `${rows[0]}${" ".repeat(gap)}${(style.timestamp ?? identity)(layout.timestamp)}`;
45
+ }
46
+ return rows.map(row => truncateToWidth(row, width));
47
+ }
48
+ function identity(text) {
49
+ return text;
50
+ }
@@ -0,0 +1,53 @@
1
+ export interface TextSelectionPoint {
2
+ readonly line: number;
3
+ readonly column: number;
4
+ }
5
+ export interface TextSelection {
6
+ readonly anchor: TextSelectionPoint;
7
+ readonly head: TextSelectionPoint;
8
+ readonly selecting: boolean;
9
+ readonly cell?: boolean;
10
+ readonly fullRow?: boolean;
11
+ /** Paint an edge whitespace run through the viewport's overlaid rail cell. */
12
+ readonly throughFinalColumn?: boolean;
13
+ }
14
+ export interface TextSelectionClick {
15
+ readonly time: number;
16
+ readonly line: number;
17
+ readonly column: number;
18
+ readonly count: number;
19
+ }
20
+ export interface TextSelectionLineContent {
21
+ readonly from?: number;
22
+ readonly to?: number;
23
+ readonly selectable?: boolean;
24
+ }
25
+ export interface OrderedTextSelection {
26
+ readonly start: TextSelectionPoint;
27
+ readonly end: TextSelectionPoint;
28
+ readonly fullRow?: boolean;
29
+ readonly throughFinalColumn?: boolean;
30
+ }
31
+ export interface TextSelectionPressInput {
32
+ readonly line: number;
33
+ readonly column: number;
34
+ readonly contentWidth: number;
35
+ readonly lineText: string;
36
+ readonly lineContent?: TextSelectionLineContent;
37
+ readonly previousClick?: TextSelectionClick;
38
+ readonly now?: number;
39
+ }
40
+ export declare const TEXT_SELECTION_MULTI_CLICK_MS = 500;
41
+ export declare function orderedTextSelection(selection: TextSelection | undefined): OrderedTextSelection | undefined;
42
+ export declare function pressTextSelection(input: TextSelectionPressInput): {
43
+ readonly selection?: TextSelection;
44
+ readonly click: TextSelectionClick;
45
+ readonly kind: "point" | "word" | "line";
46
+ };
47
+ export declare function extendTextSelection(selection: TextSelection | undefined, point: TextSelectionPoint): TextSelection | undefined;
48
+ export declare function releaseTextSelection(selection: TextSelection | undefined): TextSelection | undefined;
49
+ export declare function textSelectionLineExtendColumn(selection: TextSelection, line: number, contentWidth: number): number;
50
+ export declare function textSelectionText(selection: OrderedTextSelection, rows: readonly string[], lineContent?: (line: number) => TextSelectionLineContent | undefined): string;
51
+ /** Returns the plain cells covered by [from,to), using terminal grapheme widths. */
52
+ export declare function plainTextBetweenColumns(line: string, from: number, to: number): string;
53
+ export declare function usefulTextLineContent(line: string, from?: number): TextSelectionLineContent;
@@ -0,0 +1,168 @@
1
+ import { displayWidth, stripAnsi } from "./text.js";
2
+ const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: "grapheme" });
3
+ export const TEXT_SELECTION_MULTI_CLICK_MS = 500;
4
+ export function orderedTextSelection(selection) {
5
+ if (selection === undefined)
6
+ return undefined;
7
+ const { anchor, head } = selection;
8
+ if (anchor.line === head.line && anchor.column === head.column && !selection.cell && !selection.fullRow)
9
+ return undefined;
10
+ const anchorFirst = anchor.line < head.line || (anchor.line === head.line && anchor.column <= head.column);
11
+ const ordered = anchorFirst
12
+ ? { start: anchor, end: head }
13
+ : { start: head, end: anchor };
14
+ return {
15
+ ...ordered,
16
+ ...(selection.fullRow ? { fullRow: true } : {}),
17
+ ...(selection.throughFinalColumn ? { throughFinalColumn: true } : {}),
18
+ };
19
+ }
20
+ export function pressTextSelection(input) {
21
+ const now = input.now ?? Date.now();
22
+ const previous = input.previousClick;
23
+ const count = previous !== undefined
24
+ && now - previous.time <= TEXT_SELECTION_MULTI_CLICK_MS
25
+ && previous.line === input.line
26
+ && Math.abs(previous.column - input.column) <= 1
27
+ ? previous.count + 1
28
+ : 1;
29
+ const click = { time: now, line: input.line, column: input.column, count };
30
+ const column = clamp(input.column, 1, Math.max(1, input.contentWidth));
31
+ const kind = ((count - 1) % 3) + 1;
32
+ if (kind === 2) {
33
+ const selection = selectWord(input.line, column, input.contentWidth, input.lineText);
34
+ return selection === undefined ? { click, kind: "word" } : { selection, click, kind: "word" };
35
+ }
36
+ if (kind === 3) {
37
+ if (input.lineContent?.selectable === false)
38
+ return { click, kind: "line" };
39
+ const from = clamp(input.lineContent?.from ?? 1, 1, Math.max(1, input.contentWidth));
40
+ const to = clamp(input.lineContent?.to ?? input.contentWidth, from, Math.max(1, input.contentWidth));
41
+ return {
42
+ selection: {
43
+ anchor: { line: input.line, column: from },
44
+ head: { line: input.line, column: to },
45
+ selecting: true,
46
+ cell: true,
47
+ fullRow: true,
48
+ },
49
+ click,
50
+ kind: "line",
51
+ };
52
+ }
53
+ return {
54
+ selection: {
55
+ anchor: { line: input.line, column },
56
+ head: { line: input.line, column },
57
+ selecting: true,
58
+ cell: false,
59
+ },
60
+ click,
61
+ kind: "point",
62
+ };
63
+ }
64
+ export function extendTextSelection(selection, point) {
65
+ if (selection?.selecting !== true)
66
+ return selection;
67
+ if (selection.fullRow) {
68
+ if (point.line === selection.anchor.line && selection.head.line === selection.anchor.line)
69
+ return selection;
70
+ const edge = point.line < selection.anchor.line ? Number.MAX_SAFE_INTEGER : 1;
71
+ if (selection.anchor.column !== edge) {
72
+ return { ...selection, anchor: { line: selection.anchor.line, column: edge }, head: point };
73
+ }
74
+ }
75
+ return { ...selection, head: point };
76
+ }
77
+ export function releaseTextSelection(selection) {
78
+ if (selection === undefined)
79
+ return undefined;
80
+ const released = selection.selecting ? { ...selection, selecting: false } : selection;
81
+ return orderedTextSelection(released) === undefined ? undefined : released;
82
+ }
83
+ export function textSelectionLineExtendColumn(selection, line, contentWidth) {
84
+ return line < selection.anchor.line ? 1 : Math.max(1, contentWidth);
85
+ }
86
+ export function textSelectionText(selection, rows, lineContent) {
87
+ const parts = [];
88
+ for (let line = selection.start.line; line <= selection.end.line && line < rows.length; line += 1) {
89
+ const plain = stripAnsi(rows[line] ?? "");
90
+ const content = lineContent?.(line);
91
+ if (content?.selectable === false) {
92
+ parts.push("");
93
+ continue;
94
+ }
95
+ const baseFrom = line === selection.start.line ? selection.start.column - 1 : 0;
96
+ const baseTo = line === selection.end.line ? selection.end.column : Number.MAX_SAFE_INTEGER;
97
+ const from = Math.max(baseFrom, Math.max(0, (content?.from ?? 1) - 1));
98
+ const to = Math.max(from, Math.min(baseTo, content?.to ?? Number.MAX_SAFE_INTEGER));
99
+ parts.push(plain.slice(indexAtDisplayWidth(plain, from), indexAtDisplayWidth(plain, to)).trimEnd());
100
+ }
101
+ return parts.join("\n");
102
+ }
103
+ /** Returns the plain cells covered by [from,to), using terminal grapheme widths. */
104
+ export function plainTextBetweenColumns(line, from, to) {
105
+ const plain = stripAnsi(line);
106
+ return plain.slice(indexAtDisplayWidth(plain, from), indexAtDisplayWidth(plain, to));
107
+ }
108
+ export function usefulTextLineContent(line, from = 1) {
109
+ const start = Math.max(1, from);
110
+ const end = displayWidth(stripAnsi(line).replace(/\s+$/, "")) + 1;
111
+ return { from: start, to: Math.max(start, end) };
112
+ }
113
+ function selectWord(line, column, contentWidth, lineText) {
114
+ const plain = stripAnsi(lineText);
115
+ const segments = [];
116
+ let width = 0;
117
+ for (const { segment } of GRAPHEMES.segment(plain)) {
118
+ const cellWidth = displayWidth(segment);
119
+ if (cellWidth <= 0)
120
+ continue;
121
+ segments.push({ cls: characterClass(segment), from: width, to: width + cellWidth });
122
+ width += cellWidth;
123
+ }
124
+ const at = clamp(column, 1, Math.max(1, contentWidth)) - 1;
125
+ const index = segments.findIndex(segment => at >= segment.from && at < segment.to);
126
+ if (index < 0)
127
+ return undefined;
128
+ let low = index;
129
+ let high = index;
130
+ while (low > 0 && segments[low - 1]?.cls === segments[index]?.cls)
131
+ low -= 1;
132
+ while (high + 1 < segments.length && segments[high + 1]?.cls === segments[index]?.cls)
133
+ high += 1;
134
+ const end = segments[high]?.to ?? 1;
135
+ return {
136
+ anchor: { line, column: (segments[low]?.from ?? 0) + 1 },
137
+ head: { line, column: end },
138
+ selecting: true,
139
+ cell: true,
140
+ throughFinalColumn: segments[index]?.cls === 0 && end >= contentWidth,
141
+ };
142
+ }
143
+ function characterClass(character) {
144
+ if (/\s/u.test(character))
145
+ return 0;
146
+ if (/[\p{L}\p{N}_]/u.test(character))
147
+ return 1;
148
+ return 2;
149
+ }
150
+ function indexAtDisplayWidth(plain, column) {
151
+ if (column <= 0)
152
+ return 0;
153
+ let width = 0;
154
+ let index = 0;
155
+ for (const { segment } of GRAPHEMES.segment(plain)) {
156
+ const cellWidth = displayWidth(segment);
157
+ if (width + cellWidth > column)
158
+ break;
159
+ width += cellWidth;
160
+ index += segment.length;
161
+ if (width >= column)
162
+ break;
163
+ }
164
+ return index;
165
+ }
166
+ function clamp(value, minimum, maximum) {
167
+ return Math.max(minimum, Math.min(value, maximum));
168
+ }
@@ -6,6 +6,20 @@
6
6
  */
7
7
  /** Removes styling sequences so only visible characters remain. */
8
8
  export declare function stripAnsi(text: string): string;
9
+ export interface DisplayColumnRange {
10
+ readonly from: number;
11
+ readonly to: number;
12
+ }
13
+ /** Visible-column segment (word, whitespace, or punctuation) under a pointer column. */
14
+ export declare function displayWordColumnRange(text: string, pointerColumn: number): DisplayColumnRange | null;
15
+ export interface DisplayColumnSlice {
16
+ /** Grapheme-aligned visible bounds in the source row. */
17
+ readonly from: number;
18
+ readonly to: number;
19
+ readonly text: string;
20
+ }
21
+ /** Plain graphemes intersecting a visible-column range, expanded at wide-character edges. */
22
+ export declare function displayColumnSlice(text: string, requestedFrom: number, requestedTo: number): DisplayColumnSlice;
9
23
  /** Columns this text occupies once styling is removed. */
10
24
  export declare function displayWidth(text: string): number;
11
25
  /**
@@ -6,6 +6,7 @@
6
6
  */
7
7
  const ANSI_PATTERN = /\[[0-9;:?]*[ -/]*[@-~]|\][^]*(?:|\\)|[@-Z\\-_]/g;
8
8
  const SEGMENTER = new Intl.Segmenter(undefined, { granularity: "grapheme" });
9
+ const WORD_SEGMENTER = new Intl.Segmenter(undefined, { granularity: "word" });
9
10
  /** Removes styling sequences so only visible characters remain. */
10
11
  export function stripAnsi(text) {
11
12
  return text.replace(ANSI_PATTERN, "");
@@ -62,6 +63,45 @@ function graphemeWidth(grapheme) {
62
63
  }
63
64
  return width;
64
65
  }
66
+ /** Visible-column segment (word, whitespace, or punctuation) under a pointer column. */
67
+ export function displayWordColumnRange(text, pointerColumn) {
68
+ const plain = stripAnsi(text);
69
+ const pointer = Math.max(0, pointerColumn);
70
+ let column = 0;
71
+ for (const { segment } of WORD_SEGMENTER.segment(plain)) {
72
+ const width = displayWidth(segment);
73
+ const from = column;
74
+ const to = column + width;
75
+ if (pointer >= from && pointer < to)
76
+ return { from, to };
77
+ column = to;
78
+ }
79
+ return null;
80
+ }
81
+ /** Plain graphemes intersecting a visible-column range, expanded at wide-character edges. */
82
+ export function displayColumnSlice(text, requestedFrom, requestedTo) {
83
+ const plain = stripAnsi(text);
84
+ const from = Math.max(0, requestedFrom);
85
+ const to = Math.max(from, requestedTo);
86
+ let column = 0;
87
+ let actualFrom;
88
+ let actualTo = from;
89
+ let selected = "";
90
+ for (const { segment } of SEGMENTER.segment(plain)) {
91
+ const width = graphemeWidth(segment);
92
+ const start = column;
93
+ const end = column + width;
94
+ if (end > from && start < to) {
95
+ actualFrom ??= start;
96
+ actualTo = end;
97
+ selected += segment;
98
+ }
99
+ column = end;
100
+ if (start >= to)
101
+ break;
102
+ }
103
+ return { from: actualFrom ?? from, to: actualFrom === undefined ? from : actualTo, text: selected };
104
+ }
65
105
  /** Columns this text occupies once styling is removed. */
66
106
  export function displayWidth(text) {
67
107
  let width = 0;
@@ -0,0 +1,98 @@
1
+ import { type ScrollbarAppearance, type ScrollbarGeometry, type ScrollbarStyle } from "./scrollbar.js";
2
+ export interface TranscriptPromptAnchor {
3
+ readonly id: string;
4
+ readonly firstRow: number;
5
+ readonly lastRow: number;
6
+ /** The source first row, including its submitted-prompt styling and timestamp. */
7
+ readonly sourceRow: string;
8
+ }
9
+ export interface TranscriptViewportConfig {
10
+ readonly scrollbarAppearance: ScrollbarAppearance;
11
+ readonly scrollbarStyle: ScrollbarStyle;
12
+ }
13
+ export interface TranscriptViewportTheme {
14
+ readonly track: (text: string) => string;
15
+ readonly thumb: (text: string, active: boolean) => string;
16
+ readonly sticky: (text: string, hovered: boolean) => string;
17
+ readonly quietSticky: (text: string) => string;
18
+ readonly bottomControl: (text: string, hovered: boolean) => string;
19
+ readonly selection: (line: string, from: number, to: number) => string;
20
+ }
21
+ export interface TranscriptViewportFrameInput {
22
+ readonly documentRows: readonly string[];
23
+ readonly dockRows: readonly string[];
24
+ readonly promptAnchors: readonly TranscriptPromptAnchor[];
25
+ readonly width: number;
26
+ readonly height: number;
27
+ /**
28
+ * Optional zero-based terminal row for the floating bottom control. The shell
29
+ * supplies a stable anchor above its editor group so transient status and
30
+ * notification rows cannot make the control jump vertically.
31
+ */
32
+ readonly bottomControlRow?: number;
33
+ readonly now?: number;
34
+ readonly theme?: TranscriptViewportTheme;
35
+ }
36
+ export interface TranscriptViewportHitRegions {
37
+ readonly viewportHeight: number;
38
+ readonly rail: {
39
+ readonly column: number;
40
+ /** One-based terminal row. The one-row top inset is intentionally excluded. */
41
+ readonly rowStart: number;
42
+ readonly trackHeight: number;
43
+ readonly geometry: ScrollbarGeometry;
44
+ } | null;
45
+ readonly sticky: {
46
+ readonly row: number;
47
+ readonly target: number;
48
+ readonly width: number;
49
+ } | null;
50
+ readonly bottom: {
51
+ readonly row: number;
52
+ readonly columnStart: number;
53
+ readonly columnEnd: number;
54
+ } | null;
55
+ }
56
+ export interface TranscriptViewportFrame {
57
+ readonly rows: readonly string[];
58
+ readonly contentWidth: number;
59
+ readonly scrollTop: number;
60
+ readonly maxScroll: number;
61
+ readonly followingEnd: boolean;
62
+ readonly hits: TranscriptViewportHitRegions;
63
+ }
64
+ /**
65
+ * A1-owned viewport state. It knows only rows and semantic prompt anchors; Pi
66
+ * components and transcript payloads remain outside this neutral boundary.
67
+ */
68
+ export declare class TranscriptViewport {
69
+ #private;
70
+ get scrollTop(): number;
71
+ get maxScroll(): number;
72
+ get followingEnd(): boolean;
73
+ get newMessages(): number;
74
+ get frame(): TranscriptViewportFrame | null;
75
+ /** Counts one completed assistant reply while detached; tool rows never call this. */
76
+ noteNewMessage(): boolean;
77
+ setConfig(config: TranscriptViewportConfig): void;
78
+ noteScrollActivity(now?: number, lingerMs?: number): void;
79
+ setRailHovered(hovered: boolean): void;
80
+ setRailDragging(dragging: boolean): void;
81
+ setStickyHovered(hovered: boolean): void;
82
+ setBottomHovered(hovered: boolean): void;
83
+ get selectionActive(): boolean;
84
+ get hasSelection(): boolean;
85
+ pressSelection(column: number, viewportRow: number, now?: number): boolean;
86
+ extendSelection(column: number, viewportRow: number, now?: number, autoScroll?: boolean): boolean;
87
+ releaseSelection(): boolean;
88
+ clearSelection(): boolean;
89
+ selectedText(): string | null;
90
+ scrollBy(lines: number, now?: number): boolean;
91
+ scrollTo(position: number, now?: number): boolean;
92
+ scrollToEnd(now?: number): boolean;
93
+ /** Jumps to the pinned prompt governing this position, then earlier prompts. */
94
+ scrollToPreviousPrompt(now?: number): boolean;
95
+ reset(): void;
96
+ clearTransient(): void;
97
+ compose(input: TranscriptViewportFrameInput): TranscriptViewportFrame;
98
+ }