@timurproko/a1 0.1.8-dev.137 → 0.1.8-dev.151
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/README.md +2 -4
- package/dist/cli/capabilities.d.ts +2 -11
- package/dist/cli/capabilities.js +1 -1
- package/dist/cli/dispatch.js +3 -6
- package/dist/composition/owned-ui.js +26 -5
- package/dist/composition/settings-route-host.d.ts +1 -1
- package/dist/composition/settings-route-host.js +1 -1
- package/dist/contracts/owned-ui/model.d.ts +28 -0
- package/dist/contracts/owned-ui/validation.js +14 -0
- package/dist/features/launch/development-launch.js +11 -3
- package/dist/features/launch/index.d.ts +0 -1
- package/dist/features/launch/index.js +0 -1
- package/dist/features/launch/intent.d.ts +3 -2
- package/dist/features/launch/intent.js +1 -2
- package/dist/features/launch/prepare-launch.d.ts +0 -2
- package/dist/features/launch/prepare-launch.js +2 -9
- package/dist/features/launch/profile-paths.d.ts +1 -2
- package/dist/features/launch/profile-paths.js +2 -8
- package/dist/features/launch/runtime-selection.d.ts +2 -5
- package/dist/features/owned-ui/settings-app.js +38 -14
- package/dist/foundation/lifecycle/model.d.ts +1 -1
- package/dist/foundation/lifecycle/model.js +1 -1
- package/dist/foundation/release/bootstrap.d.ts +1 -3
- package/dist/foundation/release/bootstrap.js +1 -1
- package/dist/foundation/storage/control-store.js +37 -3
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +55 -0
- package/dist/integrations/pi/components/owned-editor-ux.js +778 -0
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +44 -6
- package/dist/integrations/pi/components/shell-footer-status.d.ts +1 -1
- package/dist/integrations/pi/components/shell-footer-status.js +14 -5
- package/dist/integrations/pi/components/shell-presenters-transcript.d.ts +3 -2
- package/dist/integrations/pi/components/shell-presenters-transcript.js +9 -8
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +41 -7
- package/dist/integrations/pi/components/shell-shared-facade.js +3 -1
- package/dist/integrations/pi/components/submitted-prompt-adapter.d.ts +13 -0
- package/dist/integrations/pi/components/submitted-prompt-adapter.js +27 -0
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.d.ts +10 -2
- package/dist/integrations/pi/components/upstream/adjacent/core/keybindings.js +36 -3
- package/dist/integrations/pi/engine/adapter.js +21 -3
- package/dist/integrations/pi/engine/session-integration.d.ts +2 -1
- package/dist/integrations/pi/engine/session-integration.js +17 -3
- package/dist/integrations/pi/session-ui/prompt-chips.d.ts +23 -0
- package/dist/integrations/pi/session-ui/prompt-chips.js +242 -0
- package/dist/integrations/pi/session-ui/route-host.d.ts +5 -0
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.d.ts +34 -5
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell-root.js +294 -45
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.d.ts +1 -1
- package/dist/integrations/pi/{owned-ui → session-ui}/session-shell.js +167 -24
- package/dist/integrations/pi/session-ui/session-viewport-controller.d.ts +39 -0
- package/dist/integrations/pi/session-ui/session-viewport-controller.js +369 -0
- package/dist/integrations/pi/session-ui/system-clipboard.d.ts +11 -0
- package/dist/integrations/pi/session-ui/system-clipboard.js +101 -0
- package/dist/integrations/pi/tui-runtime/adapter.d.ts +2 -1
- package/dist/integrations/pi/tui-runtime/adapter.js +51 -2
- package/dist/integrations/pi/tui-runtime/contracts.d.ts +4 -6
- 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 +3 -3
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/product-identity.d.ts +2 -2
- package/dist/product-identity.js +3 -4
- package/dist/product-identity.json +3 -5
- package/dist/ui/apps/contracts.d.ts +23 -1
- package/dist/ui/apps/host.js +9 -1
- package/dist/ui/components/index.d.ts +3 -0
- package/dist/ui/components/index.js +3 -0
- package/dist/ui/components/mouse.d.ts +10 -0
- package/dist/ui/components/mouse.js +23 -0
- package/dist/ui/components/prompt-row.d.ts +14 -0
- package/dist/ui/components/prompt-row.js +20 -0
- package/dist/ui/components/scrollbar.d.ts +26 -0
- package/dist/ui/components/scrollbar.js +35 -0
- package/dist/ui/components/spans.d.ts +21 -0
- package/dist/ui/components/spans.js +178 -0
- package/dist/ui/components/submitted-prompt.d.ts +23 -0
- package/dist/ui/components/submitted-prompt.js +50 -0
- package/dist/ui/components/text-selection.d.ts +53 -0
- package/dist/ui/components/text-selection.js +168 -0
- package/dist/ui/components/text.d.ts +14 -0
- package/dist/ui/components/text.js +40 -0
- package/dist/ui/components/transcript-viewport.d.ts +104 -0
- package/dist/ui/components/transcript-viewport.js +381 -0
- package/dist/ui/settings/declarations.d.ts +8 -1
- package/dist/ui/settings/declarations.js +33 -5
- package/dist/ui/settings/migrations.d.ts +1 -1
- package/dist/ui/settings/migrations.js +21 -2
- package/dist/ui/settings/sections.js +15 -8
- package/dist/ui/settings/session.d.ts +2 -0
- package/dist/ui/settings/session.js +7 -5
- package/docs/architecture/boundaries.md +3 -3
- package/docs/architecture/project-structure.md +2 -2
- package/docs/architecture/resource-and-data-policy.md +1 -1
- package/docs/architecture/toolchain.md +5 -5
- package/docs/architecture/ui-reference-provenance.md +11 -2
- package/docs/features/launch-profiles.md +7 -14
- package/docs/manual-launch-instance-acceptance.md +12 -35
- package/docs/manual-owned-ui-checkpoint.md +2 -3
- package/docs/manual-pi-boundary-candidate.md +4 -5
- package/package.json +4 -2
- package/dist/features/launch/profiles.d.ts +0 -12
- package/dist/features/launch/profiles.js +0 -26
- package/dist/integrations/pi/owned-ui/route-host.d.ts +0 -29
- /package/dist/integrations/pi/{owned-ui → session-ui}/index.d.ts +0 -0
- /package/dist/integrations/pi/{owned-ui → session-ui}/index.js +0 -0
- /package/dist/integrations/pi/{owned-ui → session-ui}/route-host.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
declare const ENVIRONMENT_KEYS: readonly ["certificationTarball", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "fixture", "fixtureInput", "fixtureToken", "inputAcknowledgement", "internalPackaging", "
|
|
1
|
+
declare const ENVIRONMENT_KEYS: readonly ["certificationTarball", "configDir", "dataDir", "databasePath", "developmentInstanceId", "developmentRoot", "endpoint", "fixture", "fixtureInput", "fixtureToken", "inputAcknowledgement", "internalPackaging", "launchProfile", "nativePi", "paneId", "piParityIntentionalMutation", "piPortRoot", "piSourceLedgerPath", "piSourceScanRoot", "probeTrace", "processGuardianPath", "profileHome", "protocolVersion", "releaseDigest", "releaseId", "releaseRoot", "releaseRunnerLabel", "runtimeDir", "structuredFlowLimits", "terminalArgumentsJson", "terminalExecutable", "terminalSessionId"];
|
|
2
2
|
declare const FILESYSTEM_KEYS: readonly ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
|
|
3
|
-
declare const STATE_KEYS: readonly ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile"
|
|
3
|
+
declare const STATE_KEYS: readonly ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile"];
|
|
4
4
|
declare const ENDPOINT_KEYS: readonly ["windowsPipeStem", "unixSocketFilename", "metadataFilename", "supervisorLogFilename", "databaseFilename"];
|
|
5
5
|
declare const MANIFEST_KEYS: readonly ["releaseFilename", "packageFilename"];
|
|
6
6
|
declare const PROTOCOL_KEYS: readonly ["namespace", "controlEnvelope", "supervisorSchema", "nativeHostSchema", "structuredAgentSchema", "controlStoreSchema", "releaseCohortSchema", "updateJournalSchema"];
|
package/dist/product-identity.js
CHANGED
|
@@ -12,7 +12,6 @@ const ENVIRONMENT_KEYS = [
|
|
|
12
12
|
"fixtureToken",
|
|
13
13
|
"inputAcknowledgement",
|
|
14
14
|
"internalPackaging",
|
|
15
|
-
"launchArgumentsJson",
|
|
16
15
|
"launchProfile",
|
|
17
16
|
"nativePi",
|
|
18
17
|
"paneId",
|
|
@@ -35,7 +34,7 @@ const ENVIRONMENT_KEYS = [
|
|
|
35
34
|
"terminalSessionId",
|
|
36
35
|
];
|
|
37
36
|
const FILESYSTEM_KEYS = ["slug", "windowsDirectory", "unixDirectory", "temporaryPrefix"];
|
|
38
|
-
const STATE_KEYS = ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile"
|
|
37
|
+
const STATE_KEYS = ["windowsControlDirectory", "unixControlDirectory", "developmentDirectory", "piAgentProfile", "piVanillaProfile"];
|
|
39
38
|
const ENDPOINT_KEYS = ["windowsPipeStem", "unixSocketFilename", "metadataFilename", "supervisorLogFilename", "databaseFilename"];
|
|
40
39
|
const MANIFEST_KEYS = ["releaseFilename", "packageFilename"];
|
|
41
40
|
const PROTOCOL_KEYS = ["namespace", "controlEnvelope", "supervisorSchema", "nativeHostSchema", "structuredAgentSchema", "controlStoreSchema", "releaseCohortSchema", "updateJournalSchema"];
|
|
@@ -64,8 +63,8 @@ export function validateProductIdentity(value) {
|
|
|
64
63
|
throw new TypeError("product display name must be non-empty without surrounding whitespace");
|
|
65
64
|
if (schema !== `${commandName}-product-identity-v1`)
|
|
66
65
|
throw new TypeError("product identity schema must derive from the command name");
|
|
67
|
-
if (filesystem.slug !== commandName || filesystem.unixDirectory !== filesystem.slug || filesystem.windowsDirectory !==
|
|
68
|
-
throw new TypeError("product filesystem identity must derive from
|
|
66
|
+
if (filesystem.slug !== commandName || filesystem.unixDirectory !== filesystem.slug || filesystem.windowsDirectory !== filesystem.slug) {
|
|
67
|
+
throw new TypeError("product filesystem identity must derive from the command name");
|
|
69
68
|
}
|
|
70
69
|
if (filesystem.temporaryPrefix !== `${filesystem.slug}-`)
|
|
71
70
|
throw new TypeError("product temporary prefix must derive from the filesystem slug");
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"packageName": "@timurproko/a1",
|
|
6
6
|
"filesystem": {
|
|
7
7
|
"slug": "a1",
|
|
8
|
-
"windowsDirectory": "
|
|
8
|
+
"windowsDirectory": "a1",
|
|
9
9
|
"unixDirectory": "a1",
|
|
10
10
|
"temporaryPrefix": "a1-"
|
|
11
11
|
},
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"fixtureToken": "A1_FIXTURE_TOKEN",
|
|
23
23
|
"inputAcknowledgement": "A1_INPUT_ACK",
|
|
24
24
|
"internalPackaging": "A1_INTERNAL_PACKAGING",
|
|
25
|
-
"launchArgumentsJson": "A1_LAUNCH_ARGUMENTS_JSON",
|
|
26
25
|
"launchProfile": "A1_LAUNCH_PROFILE",
|
|
27
26
|
"nativePi": "A1_NATIVE_PI",
|
|
28
27
|
"paneId": "A1_PANE_ID",
|
|
@@ -45,12 +44,11 @@
|
|
|
45
44
|
"terminalSessionId": "A1_TERMINAL_SESSION_ID"
|
|
46
45
|
},
|
|
47
46
|
"state": {
|
|
48
|
-
"windowsControlDirectory": "
|
|
47
|
+
"windowsControlDirectory": "a1",
|
|
49
48
|
"unixControlDirectory": "a1",
|
|
50
49
|
"developmentDirectory": "a1-development",
|
|
51
50
|
"piAgentProfile": ".a1/agent",
|
|
52
|
-
"piVanillaProfile": ".pi/agent"
|
|
53
|
-
"piSandboxProfile": ".a1/sandbox"
|
|
51
|
+
"piVanillaProfile": ".pi/agent"
|
|
54
52
|
},
|
|
55
53
|
"endpoint": {
|
|
56
54
|
"windowsPipeStem": "a1",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PaneInputResult, PaneMouseEvent, PaneRect, UiTheme } from "../components/index.js";
|
|
1
|
+
import type { PaneInputResult, PaneMouseEvent, PaneRect, RenderCacheContract, UiTheme } from "../components/index.js";
|
|
2
2
|
export interface AppSize {
|
|
3
3
|
readonly width: number;
|
|
4
4
|
readonly height: number;
|
|
@@ -27,6 +27,8 @@ export interface AppHostServices {
|
|
|
27
27
|
}
|
|
28
28
|
export interface UiApp {
|
|
29
29
|
readonly id: string;
|
|
30
|
+
/** Optional declared revisions used by the host's shared frame cache. */
|
|
31
|
+
readonly renderCache?: RenderCacheContract;
|
|
30
32
|
render(rect: PaneRect, host: AppHostServices): readonly string[];
|
|
31
33
|
onInput?(data: string, host: AppHostServices): PaneInputResult;
|
|
32
34
|
onMouse?(event: PaneMouseEvent, host: AppHostServices): PaneInputResult;
|
|
@@ -39,4 +41,24 @@ export interface AppRegistration {
|
|
|
39
41
|
readonly route: string;
|
|
40
42
|
create(): UiApp;
|
|
41
43
|
}
|
|
44
|
+
/** A neutral owned-app surface mounted by a surrounding presentation runtime. */
|
|
45
|
+
export interface UiRouteSurface {
|
|
46
|
+
readonly id: string;
|
|
47
|
+
/** Renders exactly `height` rows of at most `width` columns. */
|
|
48
|
+
render(width: number, height: number): readonly string[];
|
|
49
|
+
/** True when the key was consumed; false leaves it to the surrounding shell. */
|
|
50
|
+
handleInput(data: string): boolean;
|
|
51
|
+
/** Mouse report in surface-local coordinates. True when consumed. */
|
|
52
|
+
handleMouse(event: PaneMouseEvent): boolean;
|
|
53
|
+
isClosed(): boolean;
|
|
54
|
+
close(): void;
|
|
55
|
+
onRenderRequested(listener: () => void): void;
|
|
56
|
+
/** The surface asks to leave A1 entirely, not merely to close itself. */
|
|
57
|
+
onExitRequested(listener: () => void): void;
|
|
58
|
+
}
|
|
59
|
+
/** Route lookup seam shared by owned apps and any presentation runtime hosting them. */
|
|
60
|
+
export interface UiRouteHost {
|
|
61
|
+
claims(route: string): boolean;
|
|
62
|
+
open(route: string): UiRouteSurface | null;
|
|
63
|
+
}
|
|
42
64
|
export declare function assertAppRegistration(registration: AppRegistration): void;
|
package/dist/ui/apps/host.js
CHANGED
|
@@ -16,6 +16,7 @@ export class UiAppHost {
|
|
|
16
16
|
#theme;
|
|
17
17
|
/** When the first interrupt of a chord was seen, or null when disarmed. */
|
|
18
18
|
#interruptArmedAt = null;
|
|
19
|
+
#renderedInterruptArmed;
|
|
19
20
|
#cache = new FrameCache();
|
|
20
21
|
#app = null;
|
|
21
22
|
constructor(options) {
|
|
@@ -58,7 +59,11 @@ export class UiAppHost {
|
|
|
58
59
|
}
|
|
59
60
|
const size = this.#surface.size();
|
|
60
61
|
const rect = { width: Math.max(0, size.width), height: Math.max(0, size.height) };
|
|
61
|
-
const
|
|
62
|
+
const interruptArmed = this.interruptArmed;
|
|
63
|
+
if (this.#renderedInterruptArmed !== interruptArmed)
|
|
64
|
+
this.#cache.invalidate();
|
|
65
|
+
this.#renderedInterruptArmed = interruptArmed;
|
|
66
|
+
const lines = this.#guard(() => this.#cache.render(app, rect, () => finalizeFrame(app.render(rect, this.#services()), rect, app.id)));
|
|
62
67
|
this.#surface.present(lines ?? null);
|
|
63
68
|
}
|
|
64
69
|
handleInput(data) {
|
|
@@ -101,6 +106,9 @@ export class UiAppHost {
|
|
|
101
106
|
return { consumed: true, render: true };
|
|
102
107
|
}
|
|
103
108
|
this.#interruptArmedAt = now;
|
|
109
|
+
// Host-owned state participates in the app's next frame independently of
|
|
110
|
+
// the app's own revision contract.
|
|
111
|
+
this.#cache.invalidate();
|
|
104
112
|
return { consumed: true, render: true };
|
|
105
113
|
}
|
|
106
114
|
get interruptArmed() {
|
|
@@ -11,8 +11,11 @@ export * from "./scrollbar.js";
|
|
|
11
11
|
export * from "./shortcuts.js";
|
|
12
12
|
export * from "./spans.js";
|
|
13
13
|
export * from "./status-line.js";
|
|
14
|
+
export * from "./submitted-prompt.js";
|
|
14
15
|
export * from "./stepper.js";
|
|
16
|
+
export * from "./transcript-viewport.js";
|
|
15
17
|
export * from "./surface.js";
|
|
16
18
|
export * from "./text.js";
|
|
19
|
+
export * from "./text-selection.js";
|
|
17
20
|
export * from "./theme.js";
|
|
18
21
|
export * from "./value-menu.js";
|
|
@@ -11,8 +11,11 @@ export * from "./scrollbar.js";
|
|
|
11
11
|
export * from "./shortcuts.js";
|
|
12
12
|
export * from "./spans.js";
|
|
13
13
|
export * from "./status-line.js";
|
|
14
|
+
export * from "./submitted-prompt.js";
|
|
14
15
|
export * from "./stepper.js";
|
|
16
|
+
export * from "./transcript-viewport.js";
|
|
15
17
|
export * from "./surface.js";
|
|
16
18
|
export * from "./text.js";
|
|
19
|
+
export * from "./text-selection.js";
|
|
17
20
|
export * from "./theme.js";
|
|
18
21
|
export * from "./value-menu.js";
|
|
@@ -13,3 +13,13 @@ export interface ParsedMouseInput {
|
|
|
13
13
|
}
|
|
14
14
|
/** Extracts SGR mouse reports, leaving any keyboard input untouched. */
|
|
15
15
|
export declare function parseMouseInput(data: string): ParsedMouseInput;
|
|
16
|
+
export interface RoutedMouseInput {
|
|
17
|
+
/** Input left for the TUI after only claimed reports were removed. */
|
|
18
|
+
readonly data: string;
|
|
19
|
+
readonly consumed: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Routes every SGR report independently, preserving keyboard bytes and mouse
|
|
23
|
+
* reports the caller does not claim even when they share one physical chunk.
|
|
24
|
+
*/
|
|
25
|
+
export declare function routeMouseInput(data: string, claim: (event: PaneMouseEvent) => boolean): RoutedMouseInput;
|
|
@@ -30,6 +30,29 @@ export function parseMouseInput(data) {
|
|
|
30
30
|
rest += data.slice(index);
|
|
31
31
|
return { events: Object.freeze(events), rest };
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Routes every SGR report independently, preserving keyboard bytes and mouse
|
|
35
|
+
* reports the caller does not claim even when they share one physical chunk.
|
|
36
|
+
*/
|
|
37
|
+
export function routeMouseInput(data, claim) {
|
|
38
|
+
if (!data.includes("\u001b[<"))
|
|
39
|
+
return { data, consumed: false };
|
|
40
|
+
let output = "";
|
|
41
|
+
let index = 0;
|
|
42
|
+
let consumed = false;
|
|
43
|
+
SGR_PATTERN.lastIndex = 0;
|
|
44
|
+
for (let match = SGR_PATTERN.exec(data); match !== null; match = SGR_PATTERN.exec(data)) {
|
|
45
|
+
output += data.slice(index, match.index);
|
|
46
|
+
index = match.index + match[0].length;
|
|
47
|
+
const event = toEvent(Number.parseInt(match[1] ?? "", 10), Number.parseInt(match[2] ?? "", 10), Number.parseInt(match[3] ?? "", 10), match[4] === "m");
|
|
48
|
+
if (event !== null && claim(event))
|
|
49
|
+
consumed = true;
|
|
50
|
+
else
|
|
51
|
+
output += match[0];
|
|
52
|
+
}
|
|
53
|
+
output += data.slice(index);
|
|
54
|
+
return { data: output, consumed };
|
|
55
|
+
}
|
|
33
56
|
function toEvent(code, column, row, released) {
|
|
34
57
|
if ((code & 64) !== 0) {
|
|
35
58
|
const kind = (code & 1) === 0 ? "wheel-up" : "wheel-down";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface TimestampedPromptRowsInput {
|
|
2
|
+
readonly width: number;
|
|
3
|
+
readonly sourceTimestamp: number | Date;
|
|
4
|
+
readonly render: (contentWidth: number) => readonly string[];
|
|
5
|
+
/** Styles the reserved suffix while keeping policy independent of a vendor theme. */
|
|
6
|
+
readonly decorateSuffix?: (text: string, firstRow: boolean) => string;
|
|
7
|
+
readonly minimumWidth?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function formatPromptTimestamp(source: number | Date): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Reserves timestamp columns before wrapping, so the clock never overwrites
|
|
12
|
+
* prompt content. At narrow widths the same renderer is used without a clock.
|
|
13
|
+
*/
|
|
14
|
+
export declare function composeTimestampedPromptRows(input: TimestampedPromptRowsInput): readonly string[];
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { overlaySpan } from "./spans.js";
|
|
2
|
+
export function formatPromptTimestamp(source) {
|
|
3
|
+
const date = source instanceof Date ? source : new Date(source);
|
|
4
|
+
if (Number.isNaN(date.getTime()))
|
|
5
|
+
return null;
|
|
6
|
+
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reserves timestamp columns before wrapping, so the clock never overwrites
|
|
10
|
+
* prompt content. At narrow widths the same renderer is used without a clock.
|
|
11
|
+
*/
|
|
12
|
+
export function composeTimestampedPromptRows(input) {
|
|
13
|
+
const timestamp = formatPromptTimestamp(input.sourceTimestamp);
|
|
14
|
+
if (timestamp === null || input.width < (input.minimumWidth ?? 20))
|
|
15
|
+
return input.render(input.width);
|
|
16
|
+
const suffixWidth = timestamp.length + 2;
|
|
17
|
+
const contentWidth = Math.max(1, input.width - suffixWidth);
|
|
18
|
+
const decorate = input.decorateSuffix ?? ((text) => text);
|
|
19
|
+
return input.render(contentWidth).map((row, index) => overlaySpan(row, contentWidth, input.width, decorate(index === 0 ? ` ${timestamp}` : " ".repeat(suffixWidth), index === 0)));
|
|
20
|
+
}
|
|
@@ -19,6 +19,30 @@ export interface ScrollbarInput {
|
|
|
19
19
|
readonly scroll: number;
|
|
20
20
|
readonly trackHeight: number;
|
|
21
21
|
}
|
|
22
|
+
export type ScrollbarAppearance = "auto" | "always" | "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 uses the same configured rate as a wheel event. */
|
|
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 uses the same configured rate as a wheel event. */
|
|
11
|
+
export function scrollbarSelectionRows(speed) {
|
|
12
|
+
return scrollbarWheelRows(speed);
|
|
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, auto/always 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,24 @@
|
|
|
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
|
+
/** Wraps visible columns `[from,to)` in an explicit OSC 8 hyperlink. */
|
|
14
|
+
export declare function hyperlinkSgrSpan(line: string, from: number, to: number, target: string, widthOf?: (text: string) => number): string;
|
|
15
|
+
/**
|
|
16
|
+
* Gives existing and bare web links explicit, tightly bounded OSC 8 regions so
|
|
17
|
+
* the terminal owns dotted-idle/solid-hover decoration without leaking hover
|
|
18
|
+
* state into cells repainted during scrolling.
|
|
19
|
+
*/
|
|
20
|
+
export declare function nativeHyperlinkStyle(line: string, color?: (text: string, target: string) => string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Held-button paint for terminal-native links. OSC 8 is removed so the terminal
|
|
23
|
+
* cannot switch a selected link to its solid hover underline. A dotted SGR
|
|
24
|
+
* underline preserves the idle appearance, and VS15 transiently breaks native
|
|
25
|
+
* plain-text URL detection without changing visible width.
|
|
26
|
+
*/
|
|
27
|
+
export declare function heldNativeHyperlinkStyle(line: string): string;
|
|
28
|
+
export declare function backgroundSgrSpan(line: string, from: number, to: number, on?: string, off?: string, widthOf?: (text: string) => number): string;
|
|
@@ -9,6 +9,9 @@ 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$/;
|
|
13
|
+
const BARE_URL = /https?:\/\/[^\s\u0000-\u001f\u007f<>"']+/giu;
|
|
14
|
+
const TRAILING_URL_PUNCTUATION = /[.,;:!?]/u;
|
|
12
15
|
const GRAPHEMES = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
13
16
|
/**
|
|
14
17
|
* Replaces the visible columns `[from, to)` of `line` with `span`, preserving
|
|
@@ -57,3 +60,178 @@ export function overlaySpan(line, from, to, span) {
|
|
|
57
60
|
head += " ".repeat(from - column);
|
|
58
61
|
return head + (hyperlinkOpen ? HYPERLINK_OFF : "") + span + replay + tail;
|
|
59
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Paints only a background over [from,to), preserving every glyph and its
|
|
65
|
+
* foreground, bold, underline, and hyperlink styling. Embedded resets reapply
|
|
66
|
+
* the selection background, while leaving the span restores the row's styles.
|
|
67
|
+
*/
|
|
68
|
+
/** Wraps visible columns `[from,to)` in an explicit OSC 8 hyperlink. */
|
|
69
|
+
export function hyperlinkSgrSpan(line, from, to, target, widthOf = displayWidth) {
|
|
70
|
+
const on = `\u001b]8;;${target}\u001b\\`;
|
|
71
|
+
const off = "\u001b]8;;\u001b\\";
|
|
72
|
+
let output = "";
|
|
73
|
+
let column = 0;
|
|
74
|
+
let inside = false;
|
|
75
|
+
for (const token of line.split(ANSI_SPLIT)) {
|
|
76
|
+
if (!token)
|
|
77
|
+
continue;
|
|
78
|
+
if (token.startsWith("\u001b")) {
|
|
79
|
+
output += token;
|
|
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;
|
|
86
|
+
inside = shouldBeInside;
|
|
87
|
+
}
|
|
88
|
+
output += segment;
|
|
89
|
+
column += widthOf(segment);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (inside)
|
|
93
|
+
output += off;
|
|
94
|
+
return output;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Gives existing and bare web links explicit, tightly bounded OSC 8 regions so
|
|
98
|
+
* the terminal owns dotted-idle/solid-hover decoration without leaking hover
|
|
99
|
+
* state into cells repainted during scrolling.
|
|
100
|
+
*/
|
|
101
|
+
export function nativeHyperlinkStyle(line, color = text => text) {
|
|
102
|
+
let hyperlinkTarget;
|
|
103
|
+
let sgrReplay = "";
|
|
104
|
+
let output = "";
|
|
105
|
+
for (const token of line.split(ANSI_SPLIT)) {
|
|
106
|
+
if (!token)
|
|
107
|
+
continue;
|
|
108
|
+
const link = HYPERLINK.exec(token);
|
|
109
|
+
if (link) {
|
|
110
|
+
hyperlinkTarget = (link[1] ?? "") || undefined;
|
|
111
|
+
output += token;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!token.startsWith("\u001b")) {
|
|
115
|
+
output += hyperlinkTarget === undefined
|
|
116
|
+
? decorateBareUrls(token, color, sgrReplay)
|
|
117
|
+
: color(token, hyperlinkTarget);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (SGR.test(token)) {
|
|
121
|
+
if (token === "\u001b[0m")
|
|
122
|
+
sgrReplay = token;
|
|
123
|
+
else
|
|
124
|
+
sgrReplay += token;
|
|
125
|
+
}
|
|
126
|
+
if (hyperlinkTarget === undefined || (token !== "\u001b[4m" && token !== "\u001b[24m"))
|
|
127
|
+
output += token;
|
|
128
|
+
}
|
|
129
|
+
return hyperlinkTarget === undefined ? output : output + HYPERLINK_OFF;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Held-button paint for terminal-native links. OSC 8 is removed so the terminal
|
|
133
|
+
* cannot switch a selected link to its solid hover underline. A dotted SGR
|
|
134
|
+
* underline preserves the idle appearance, and VS15 transiently breaks native
|
|
135
|
+
* plain-text URL detection without changing visible width.
|
|
136
|
+
*/
|
|
137
|
+
export function heldNativeHyperlinkStyle(line) {
|
|
138
|
+
let hyperlinkTarget;
|
|
139
|
+
let fileDetectorBroken = false;
|
|
140
|
+
let output = "";
|
|
141
|
+
for (const token of line.split(ANSI_SPLIT)) {
|
|
142
|
+
if (!token)
|
|
143
|
+
continue;
|
|
144
|
+
const link = HYPERLINK.exec(token);
|
|
145
|
+
if (link) {
|
|
146
|
+
const nextTarget = (link[1] ?? "") || undefined;
|
|
147
|
+
if ((nextTarget !== undefined) !== (hyperlinkTarget !== undefined)) {
|
|
148
|
+
output += nextTarget === undefined ? "\u001b[24m" : "\u001b[4:4m";
|
|
149
|
+
}
|
|
150
|
+
if (nextTarget !== hyperlinkTarget)
|
|
151
|
+
fileDetectorBroken = false;
|
|
152
|
+
hyperlinkTarget = nextTarget;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (token.startsWith("\u001b")) {
|
|
156
|
+
output += token;
|
|
157
|
+
// Foreground helpers may reset every SGR attribute; restore the held
|
|
158
|
+
// dotted underline without disturbing the source colour or intensity.
|
|
159
|
+
if (hyperlinkTarget !== undefined && SGR.test(token))
|
|
160
|
+
output += "\u001b[4:4m";
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (hyperlinkTarget === undefined) {
|
|
164
|
+
output += token;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
let painted = token.replaceAll("https://", "https:\uFE0E//").replaceAll("http://", "http:\uFE0E//");
|
|
168
|
+
// Windows Terminal also detects file-link labels such as package.json and
|
|
169
|
+
// D:/work/file.ts after OSC 8 is removed. Break that detector once inside
|
|
170
|
+
// the paint-only label; semantic transcript text remains untouched.
|
|
171
|
+
if (!fileDetectorBroken && /^file:/iu.test(hyperlinkTarget)) {
|
|
172
|
+
const firstVisible = /\S/u.exec(painted);
|
|
173
|
+
if (firstVisible?.index !== undefined) {
|
|
174
|
+
const index = firstVisible.index;
|
|
175
|
+
const codePoint = painted.codePointAt(index) ?? 0;
|
|
176
|
+
const end = index + (codePoint > 0xffff ? 2 : 1);
|
|
177
|
+
painted = `${painted.slice(0, end)}\uFE0E${painted.slice(end)}`;
|
|
178
|
+
fileDetectorBroken = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
output += painted;
|
|
182
|
+
}
|
|
183
|
+
return hyperlinkTarget === undefined ? output : `${output}\u001b[24m`;
|
|
184
|
+
}
|
|
185
|
+
function decorateBareUrls(text, color, sgrReplay) {
|
|
186
|
+
return text.replace(BARE_URL, candidate => {
|
|
187
|
+
const { target, trailing } = splitUrlTrailingPunctuation(candidate);
|
|
188
|
+
if (target.length === 0)
|
|
189
|
+
return candidate;
|
|
190
|
+
return `\u001b]8;;${target}\u001b\\\u001b[24m${color(target, target)}${HYPERLINK_OFF}${sgrReplay}${trailing}`;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function splitUrlTrailingPunctuation(candidate) {
|
|
194
|
+
let end = candidate.length;
|
|
195
|
+
while (end > 0 && TRAILING_URL_PUNCTUATION.test(candidate[end - 1] ?? ""))
|
|
196
|
+
end -= 1;
|
|
197
|
+
for (const [opening, closing] of [["(", ")"], ["[", "]"], ["{", "}"]]) {
|
|
198
|
+
while (candidate.slice(0, end).endsWith(closing)
|
|
199
|
+
&& occurrences(candidate.slice(0, end), closing) > occurrences(candidate.slice(0, end), opening)) {
|
|
200
|
+
end -= 1;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return { target: candidate.slice(0, end), trailing: candidate.slice(end) };
|
|
204
|
+
}
|
|
205
|
+
function occurrences(text, value) {
|
|
206
|
+
return text.split(value).length - 1;
|
|
207
|
+
}
|
|
208
|
+
export function backgroundSgrSpan(line, from, to, on = "\u001b[47m", off = "\u001b[49m", widthOf = displayWidth) {
|
|
209
|
+
let output = "";
|
|
210
|
+
let seen = "";
|
|
211
|
+
let column = 0;
|
|
212
|
+
let inside = false;
|
|
213
|
+
for (const token of line.split(ANSI_SPLIT)) {
|
|
214
|
+
if (!token)
|
|
215
|
+
continue;
|
|
216
|
+
if (token.startsWith("\u001b")) {
|
|
217
|
+
output += token;
|
|
218
|
+
if (SGR.test(token))
|
|
219
|
+
seen += token;
|
|
220
|
+
if (inside)
|
|
221
|
+
output += on;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
for (const { segment } of GRAPHEMES.segment(token)) {
|
|
225
|
+
const shouldBeInside = column >= from && column < to;
|
|
226
|
+
if (shouldBeInside !== inside) {
|
|
227
|
+
output += shouldBeInside ? on : off + seen;
|
|
228
|
+
inside = shouldBeInside;
|
|
229
|
+
}
|
|
230
|
+
output += segment;
|
|
231
|
+
column += widthOf(segment);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (inside)
|
|
235
|
+
output += off;
|
|
236
|
+
return output;
|
|
237
|
+
}
|
|
@@ -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
|
+
}
|