@cueloop/client 0.1.0-alpha.62 → 0.1.0-alpha.64
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -6
- package/src/App.toast.test.tsx +18 -15
- package/src/App.tsx +14 -3
- package/src/app-view-model.ts +3 -13
- package/src/components/DiffSheet.scroll.test.tsx +5 -2
- package/src/components/DiffSheet.tsx +1 -6
- package/src/components/PlanSheet.tsx +1 -4
- package/src/components/PrototypeSheet.tsx +5 -9
- package/src/components/ReviewRail.tsx +3 -1
- package/src/components/plan-sheet-run-style.ts +8 -10
- package/src/components/primitives/Tabs.tsx +1 -1
- package/src/components/story-fixtures.ts +6 -2
- package/src/components/story.ts +16 -4
- package/src/components/syntax-highlight.ts +10 -2
- package/src/components/terminal-pane.test.tsx +1 -1
- package/src/components/terminal-pane.ts +7 -1
- package/src/config.ts +112 -99
- package/src/editor.ts +5 -1
- package/src/ghostty-terminal.test.ts +1 -1
- package/src/herdr.ts +10 -2
- package/src/intent-dispatch.test.ts +100 -53
- package/src/intent-dispatch.ts +12 -10
- package/src/key-bindings.ts +16 -7
- package/src/keymap.test.ts +5 -7
- package/src/kitty-image.test.ts +3 -3
- package/src/prototype-browser.ts +22 -12
- package/src/serve.ts +1 -1
- package/src/session-controller.curate.test.ts +18 -3
- package/src/session-controller.share.test.ts +28 -18
- package/src/session-controller.ts +49 -27
- package/src/share.ts +3 -1
- package/src/test-support.ts +5 -1
- package/src/theme-presets.ts +9 -2
- package/src/view-diff.ts +1 -4
- package/src/view-plan.ts +6 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/client",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.64",
|
|
4
4
|
"description": "The cueloop review UI: projection renderer, keyboard grammar, review rail",
|
|
5
5
|
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@cueloop/daemon": "0.1.0-alpha.
|
|
30
|
-
"@cueloop/extension-api": "0.1.0-alpha.
|
|
31
|
-
"@cueloop/integration-obsidian": "0.1.0-alpha.
|
|
32
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
29
|
+
"@cueloop/daemon": "0.1.0-alpha.64",
|
|
30
|
+
"@cueloop/extension-api": "0.1.0-alpha.64",
|
|
31
|
+
"@cueloop/integration-obsidian": "0.1.0-alpha.64",
|
|
32
|
+
"@cueloop/schema": "0.1.0-alpha.64",
|
|
33
33
|
"@opentui/core": "^0.5.8",
|
|
34
34
|
"@opentui/keymap": "0.5.8",
|
|
35
35
|
"@opentui/react": "^0.5.8",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"@pierre/diffs": "^1.3.6",
|
|
38
38
|
"diff": "^9",
|
|
39
39
|
"puppeteer-core": "^25.8.0",
|
|
40
|
-
"react": "^19.2.0"
|
|
40
|
+
"react": "^19.2.0",
|
|
41
|
+
"valibot": "^1.4.2"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@types/react": "^19.2.0",
|
package/src/App.toast.test.tsx
CHANGED
|
@@ -8,20 +8,20 @@ import React from "react";
|
|
|
8
8
|
import { testRender } from "@opentui/react/test-utils";
|
|
9
9
|
import { DaemonServer } from "@cueloop/daemon";
|
|
10
10
|
import type { ReviewSession } from "@cueloop/schema";
|
|
11
|
+
import { App } from "./App";
|
|
12
|
+
import type { ShareTransport } from "./session-controller";
|
|
13
|
+
import { isolateUserConfig, press, waitForText, waitForTextGone } from "./test-support";
|
|
11
14
|
|
|
12
|
-
// the controller reaches the gateway through "./share"; stub it so `S` yields a toast
|
|
13
15
|
const publishShare = mock(async () => ({ line: "ssh p_share01@cueloop.dev", copied: true }));
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const shareTransport: ShareTransport = {
|
|
17
|
+
publish: publishShare,
|
|
18
|
+
pull: mock(async () => {
|
|
19
|
+
throw new Error("Unexpected share pull");
|
|
20
|
+
}),
|
|
21
|
+
push: mock(async () => {}),
|
|
22
|
+
parseShareId: (line) => line.match(/^ssh (\S+)@/)?.[1],
|
|
20
23
|
collaboratorAnnotations: () => [],
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const { App } = await import("./App");
|
|
24
|
-
const { isolateUserConfig, press, waitForText, waitForTextGone } = await import("./test-support");
|
|
24
|
+
};
|
|
25
25
|
|
|
26
26
|
const PLAN = `# Migration Plan\n\n## Context\n\nThe daemon persists sessions to disk atomically.\n`;
|
|
27
27
|
|
|
@@ -53,10 +53,13 @@ afterEach(() => {
|
|
|
53
53
|
describe("share toast", () => {
|
|
54
54
|
test("escape cancels an open composer even while the toast is up", async () => {
|
|
55
55
|
// Arrange
|
|
56
|
-
const setup = await testRender(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const setup = await testRender(
|
|
57
|
+
<App home={home} sessionId={session.id} shareTransport={shareTransport} />,
|
|
58
|
+
{
|
|
59
|
+
width: 120,
|
|
60
|
+
height: 32,
|
|
61
|
+
},
|
|
62
|
+
);
|
|
60
63
|
|
|
61
64
|
await waitForText(setup, "cueloop");
|
|
62
65
|
|
package/src/App.tsx
CHANGED
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
type ThemeName,
|
|
38
38
|
} from "./theme-presets";
|
|
39
39
|
import type { Theme } from "./theme";
|
|
40
|
-
import { createReviewController } from "./session-controller";
|
|
40
|
+
import { createReviewController, type ShareTransport } from "./session-controller";
|
|
41
41
|
import type { SessionClient } from "@cueloop/daemon/client";
|
|
42
42
|
import { launchHarnessInSplit } from "@cueloop/daemon/herdr-split";
|
|
43
43
|
import { activeSpanState, createIntentDispatch, type Mode } from "./intent-dispatch";
|
|
@@ -105,6 +105,7 @@ export interface AppProps {
|
|
|
105
105
|
clock?: Clock;
|
|
106
106
|
/** Session source; the sharing gateway injects a blob-backed client. */
|
|
107
107
|
openClient?: () => Promise<SessionClient>;
|
|
108
|
+
shareTransport?: ShareTransport;
|
|
108
109
|
/**
|
|
109
110
|
* Who is at the keyboard. `owner` is the local planner (default). `observer`
|
|
110
111
|
* is a passive `cueloop serve` watcher (read-only). `collaborator` is a share
|
|
@@ -131,6 +132,7 @@ export function App({
|
|
|
131
132
|
onExit,
|
|
132
133
|
clock,
|
|
133
134
|
openClient,
|
|
135
|
+
shareTransport,
|
|
134
136
|
role = "owner",
|
|
135
137
|
selfAuthor,
|
|
136
138
|
appearance = "dark",
|
|
@@ -138,7 +140,15 @@ export function App({
|
|
|
138
140
|
const { observer, isOwner } = computeRoleCapabilities(readOnly, role);
|
|
139
141
|
const controller = useMemo(
|
|
140
142
|
() =>
|
|
141
|
-
createReviewController({
|
|
143
|
+
createReviewController({
|
|
144
|
+
home,
|
|
145
|
+
sessionId,
|
|
146
|
+
readOnly: observer,
|
|
147
|
+
onExit,
|
|
148
|
+
clock,
|
|
149
|
+
openClient,
|
|
150
|
+
shareTransport,
|
|
151
|
+
}),
|
|
142
152
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
143
153
|
[home, sessionId],
|
|
144
154
|
);
|
|
@@ -305,7 +315,8 @@ export function App({
|
|
|
305
315
|
if (!session) return positions;
|
|
306
316
|
if (isDiff) {
|
|
307
317
|
for (const annotation of session.annotations) {
|
|
308
|
-
const blockIndex =
|
|
318
|
+
const blockIndex =
|
|
319
|
+
"blockIndex" in annotation.anchor ? annotation.anchor.blockIndex : undefined;
|
|
309
320
|
|
|
310
321
|
if (blockIndex !== undefined) positions.set(annotation.id, blockIndex);
|
|
311
322
|
}
|
package/src/app-view-model.ts
CHANGED
|
@@ -18,17 +18,13 @@ import { REVIEW_COMPACT_WIDTH, resolveReviewWidth, type ReviewPanelMode } from "
|
|
|
18
18
|
export function computeRoleCapabilities(
|
|
19
19
|
readOnly: boolean,
|
|
20
20
|
role: "owner" | "observer" | "collaborator",
|
|
21
|
-
)
|
|
21
|
+
) {
|
|
22
22
|
const observer = readOnly || role === "observer";
|
|
23
23
|
|
|
24
24
|
return { observer, isOwner: !observer && role === "owner" };
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function deriveReviewFlags(session: ReviewSession | null)
|
|
28
|
-
isDiff: boolean;
|
|
29
|
-
isPrototype: boolean;
|
|
30
|
-
resolved: boolean;
|
|
31
|
-
} {
|
|
27
|
+
export function deriveReviewFlags(session: ReviewSession | null) {
|
|
32
28
|
return {
|
|
33
29
|
isDiff: session?.artifact.type === "diff",
|
|
34
30
|
isPrototype: session?.artifact.type === "prototype",
|
|
@@ -121,13 +117,7 @@ export function buildRenderFlags(params: {
|
|
|
121
117
|
menuOpen: boolean;
|
|
122
118
|
menuDialog: "keybinds" | "settings" | null;
|
|
123
119
|
resolvedIds: Set<string>;
|
|
124
|
-
})
|
|
125
|
-
showOwnerActions: boolean;
|
|
126
|
-
prototypeCanComment: boolean;
|
|
127
|
-
chromeHidden: boolean;
|
|
128
|
-
prototypePath: string;
|
|
129
|
-
railResolvedIds: Set<string> | null;
|
|
130
|
-
} {
|
|
120
|
+
}) {
|
|
131
121
|
const { session, isOwner, isDiff, isPrototype, resolved, menuOpen, menuDialog, resolvedIds } =
|
|
132
122
|
params;
|
|
133
123
|
|
|
@@ -4,7 +4,7 @@ import { expect, test } from "bun:test";
|
|
|
4
4
|
import React, { useState } from "react";
|
|
5
5
|
import { testRender } from "@opentui/react/test-utils";
|
|
6
6
|
import { useKeyboard } from "@opentui/react";
|
|
7
|
-
import
|
|
7
|
+
import { ScrollBoxRenderable, type Renderable } from "@opentui/core";
|
|
8
8
|
import type { Annotation } from "@cueloop/schema";
|
|
9
9
|
import { DiffSheet } from "./DiffSheet";
|
|
10
10
|
import { diffRows, type DiffRow } from "../view-diff";
|
|
@@ -82,7 +82,10 @@ test("scrolling down past wrapped annotation cards keeps the cursor pinned and t
|
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
await setup.waitForVisualIdle();
|
|
85
|
-
const
|
|
85
|
+
const found = findById(setup.renderer.root, "diff-scroll");
|
|
86
|
+
|
|
87
|
+
if (!(found instanceof ScrollBoxRenderable)) throw new Error("diff-scroll is not a scrollbox");
|
|
88
|
+
const scrollbox = found;
|
|
86
89
|
|
|
87
90
|
// the cursor row is the one painted with the cursor background
|
|
88
91
|
const cursorScreenRow = (): number => {
|
|
@@ -60,12 +60,7 @@ export interface DiffSheetProps {
|
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
/** The sign, base color, and background a row draws with. */
|
|
63
|
-
function rowStyle(
|
|
64
|
-
row: DiffRow,
|
|
65
|
-
isCursorRow: boolean,
|
|
66
|
-
isAnnotatedRow: boolean,
|
|
67
|
-
tokens: Theme,
|
|
68
|
-
): { sign: string; baseColor: string; background: string | undefined } {
|
|
63
|
+
function rowStyle(row: DiffRow, isCursorRow: boolean, isAnnotatedRow: boolean, tokens: Theme) {
|
|
69
64
|
const sign = row.kind === "add" ? "+" : row.kind === "del" ? "-" : " ";
|
|
70
65
|
const baseColor =
|
|
71
66
|
row.kind === "add"
|
|
@@ -94,10 +94,7 @@ interface BlockRef {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
/** Screen position of a rendered-text offset inside a wrapped text. */
|
|
97
|
-
function positionOfRenderedOffset(
|
|
98
|
-
renderable: TextRenderable,
|
|
99
|
-
renderedOffset: number,
|
|
100
|
-
): { x: number; y: number } {
|
|
97
|
+
function positionOfRenderedOffset(renderable: TextRenderable, renderedOffset: number) {
|
|
101
98
|
const info = renderable.lineInfo;
|
|
102
99
|
let lineIndex = 0;
|
|
103
100
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import React, { useEffect, useRef, useState } from "react";
|
|
12
12
|
import type { BoxRenderable } from "@opentui/core";
|
|
13
|
+
import * as v from "valibot";
|
|
13
14
|
import { useKeyboard, useRenderer } from "@opentui/react";
|
|
14
15
|
import type { Theme } from "../theme";
|
|
15
16
|
import { quickActionBody, type QuickAction } from "../config";
|
|
@@ -95,10 +96,7 @@ type CaptureRenderer = {
|
|
|
95
96
|
* raster and fewer bytes). Without a resolution report, fall back to a fixed
|
|
96
97
|
* width at 2x and derive the height from the cell aspect.
|
|
97
98
|
*/
|
|
98
|
-
function captureConfig(
|
|
99
|
-
region: CellRegion,
|
|
100
|
-
renderer: CaptureRenderer,
|
|
101
|
-
): { viewport: PrototypeViewport; deviceScaleFactor: number } {
|
|
99
|
+
function captureConfig(region: CellRegion, renderer: CaptureRenderer) {
|
|
102
100
|
const resolution = renderer.resolution;
|
|
103
101
|
|
|
104
102
|
if (resolution && renderer.terminalWidth >= 1 && renderer.terminalHeight >= 1) {
|
|
@@ -174,11 +172,9 @@ function PrototypeSheetImpl({
|
|
|
174
172
|
// harness), but the launch trigger still runs.
|
|
175
173
|
useEffect(() => {
|
|
176
174
|
if (!renderer) return;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
const write =
|
|
181
|
-
typeof rawWrite === "function" ? (chunk: string) => rawWrite.call(renderer, chunk) : null;
|
|
175
|
+
const rendererOutput = v.safeParse(v.object({ writeOut: v.function() }), renderer);
|
|
176
|
+
const rawWrite = rendererOutput.success ? rendererOutput.output.writeOut : null;
|
|
177
|
+
const write = rawWrite ? (chunk: string) => rawWrite.call(renderer, chunk) : null;
|
|
182
178
|
const medium = resolveTransmitMedium();
|
|
183
179
|
const paint = (): void => {
|
|
184
180
|
if (!write) return;
|
|
@@ -187,7 +187,9 @@ export const ReviewRail = forwardRef<ReviewRailHandle, ReviewRailProps>(function
|
|
|
187
187
|
>
|
|
188
188
|
<Tabs
|
|
189
189
|
selectedKey={railTab}
|
|
190
|
-
onSelectionChange={(key) =>
|
|
190
|
+
onSelectionChange={(key) => {
|
|
191
|
+
if (key === "review" || key === "agent") onTabChange(key);
|
|
192
|
+
}}
|
|
191
193
|
theme={theme}
|
|
192
194
|
>
|
|
193
195
|
<TabList>
|
|
@@ -44,11 +44,13 @@ function blockBaseStyle(block: DisplayBlock, tokens: Theme): BlockBaseStyle {
|
|
|
44
44
|
return { baseFg, baseAttributes };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
interface PlanSheetRunStyle {
|
|
48
|
+
fg?: string;
|
|
49
|
+
bg?: string;
|
|
50
|
+
attributes?: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function inlineRoleStyle(run: StyleRun, base: BlockBaseStyle, tokens: Theme): PlanSheetRunStyle {
|
|
52
54
|
const { baseFg, baseAttributes } = base;
|
|
53
55
|
const withBase = (roleAttributes: number): number => baseAttributes | roleAttributes;
|
|
54
56
|
|
|
@@ -78,11 +80,7 @@ function inlineRoleStyle(
|
|
|
78
80
|
}
|
|
79
81
|
}
|
|
80
82
|
|
|
81
|
-
export function runStyle(
|
|
82
|
-
run: StyleRun,
|
|
83
|
-
block: DisplayBlock,
|
|
84
|
-
tokens: Theme,
|
|
85
|
-
): { fg?: string; bg?: string; attributes?: number } {
|
|
83
|
+
export function runStyle(run: StyleRun, block: DisplayBlock, tokens: Theme): PlanSheetRunStyle {
|
|
86
84
|
// a cut block reads as removed: every run struck through and grayed, never red
|
|
87
85
|
if (block.type === "del") return { fg: tokens.textDim, attributes: CUT_ATTRIBUTES };
|
|
88
86
|
|
|
@@ -65,7 +65,7 @@ export function TabList({ children }: TabListProps): React.ReactNode {
|
|
|
65
65
|
const items: { id: string; label: string }[] = [];
|
|
66
66
|
|
|
67
67
|
React.Children.forEach(children, (child) => {
|
|
68
|
-
if (React.isValidElement<TabProps>(child)
|
|
68
|
+
if (React.isValidElement<TabProps>(child)) {
|
|
69
69
|
items.push({ id: child.props.id, label: child.props.children });
|
|
70
70
|
}
|
|
71
71
|
});
|
|
@@ -63,9 +63,11 @@ export function fixturePlanSession(overrides: Partial<ReviewSession> = {}): Revi
|
|
|
63
63
|
},
|
|
64
64
|
revisions: [{ revision: 1, content: FIXTURE_PLAN, submittedAt: "2026-01-01T00:00:00Z" }],
|
|
65
65
|
annotations: FIXTURE_ANNOTATIONS,
|
|
66
|
+
verdict: null,
|
|
66
67
|
status: "pending",
|
|
68
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
67
69
|
...overrides,
|
|
68
|
-
}
|
|
70
|
+
};
|
|
69
71
|
}
|
|
70
72
|
|
|
71
73
|
export function fixtureDiffSession(overrides: Partial<ReviewSession> = {}): ReviewSession {
|
|
@@ -80,9 +82,11 @@ export function fixtureDiffSession(overrides: Partial<ReviewSession> = {}): Revi
|
|
|
80
82
|
},
|
|
81
83
|
revisions: [{ revision: 1, content: FIXTURE_PATCH, submittedAt: "2026-01-01T00:00:00Z" }],
|
|
82
84
|
annotations: [],
|
|
85
|
+
verdict: null,
|
|
83
86
|
status: "pending",
|
|
87
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
84
88
|
...overrides,
|
|
85
|
-
}
|
|
89
|
+
};
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
export function fixtureDisplay() {
|
package/src/components/story.ts
CHANGED
|
@@ -32,7 +32,19 @@ export interface LoadedStory {
|
|
|
32
32
|
|
|
33
33
|
export function isStory(value: unknown): value is Story {
|
|
34
34
|
return (
|
|
35
|
-
typeof value === "object" &&
|
|
35
|
+
typeof value === "object" &&
|
|
36
|
+
value !== null &&
|
|
37
|
+
"render" in value &&
|
|
38
|
+
typeof value.render === "function"
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isStoryMeta(value: unknown): value is StoryMeta {
|
|
43
|
+
return (
|
|
44
|
+
typeof value === "object" &&
|
|
45
|
+
value !== null &&
|
|
46
|
+
"title" in value &&
|
|
47
|
+
typeof value.title === "string"
|
|
36
48
|
);
|
|
37
49
|
}
|
|
38
50
|
|
|
@@ -43,9 +55,9 @@ export async function loadStories(): Promise<LoadedStory[]> {
|
|
|
43
55
|
const loaded: LoadedStory[] = [];
|
|
44
56
|
|
|
45
57
|
for (const file of files) {
|
|
46
|
-
const moduleExports =
|
|
47
|
-
const meta = moduleExports["meta"]
|
|
48
|
-
const moduleTitle = meta
|
|
58
|
+
const moduleExports = await import(`${import.meta.dir}/${file}`);
|
|
59
|
+
const meta = moduleExports["meta"];
|
|
60
|
+
const moduleTitle = isStoryMeta(meta) ? meta.title : file.replace(/\.stories\.tsx$/, "");
|
|
49
61
|
|
|
50
62
|
for (const [exportName, exported] of Object.entries(moduleExports)) {
|
|
51
63
|
if (exportName === "meta" || !isStory(exported)) continue;
|
|
@@ -19,7 +19,7 @@ export function filetypeFor(language?: string): string | undefined {
|
|
|
19
19
|
const info = (language ?? "").toLowerCase();
|
|
20
20
|
|
|
21
21
|
if (!info) return undefined;
|
|
22
|
-
const aliases:
|
|
22
|
+
const aliases: SyntaxAliases = {
|
|
23
23
|
ts: "typescript",
|
|
24
24
|
js: "javascript",
|
|
25
25
|
py: "python",
|
|
@@ -47,13 +47,21 @@ export function filetypeForPath(path: string): string | undefined {
|
|
|
47
47
|
return extensionToFiletype.get(basename.slice(dotIndex + 1).toLowerCase());
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
interface SyntaxAliases {
|
|
51
|
+
[alias: string]: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
50
54
|
interface SyntaxGroupStyle {
|
|
51
55
|
fg: string;
|
|
52
56
|
italic?: boolean;
|
|
53
57
|
}
|
|
54
58
|
|
|
59
|
+
interface SyntaxGroupStyles {
|
|
60
|
+
[group: string]: SyntaxGroupStyle;
|
|
61
|
+
}
|
|
62
|
+
|
|
55
63
|
/** The one group -> style map, keyed by tree-sitter capture name. */
|
|
56
|
-
function syntaxGroupStyles(theme: Theme):
|
|
64
|
+
function syntaxGroupStyles(theme: Theme): SyntaxGroupStyles {
|
|
57
65
|
return {
|
|
58
66
|
default: { fg: theme.textMuted },
|
|
59
67
|
comment: { fg: theme.textDim, italic: true },
|
|
@@ -34,7 +34,7 @@ describe("TerminalPaneRenderable", () => {
|
|
|
34
34
|
|
|
35
35
|
test("exposes availability + the renderable class", () => {
|
|
36
36
|
// Assert - the module loads and reports platform support honestly
|
|
37
|
-
expect(
|
|
37
|
+
expect([true, false]).toContain(embeddedTerminalAvailable());
|
|
38
38
|
expect(TerminalPaneRenderable).toBeDefined();
|
|
39
39
|
});
|
|
40
40
|
});
|
|
@@ -47,6 +47,12 @@ export interface TerminalPaneOptions extends RenderableOptions {
|
|
|
47
47
|
onExit?: (exitCode: number) => void;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
function terminalEnvironment(env: NodeJS.ProcessEnv) {
|
|
51
|
+
return Object.fromEntries(
|
|
52
|
+
Object.entries(env).filter((entry): entry is [string, string] => entry[1] !== undefined),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
const encoder = new TextEncoder();
|
|
51
57
|
const DEFAULT_FG = RGBA.fromInts(208, 208, 208, 255);
|
|
52
58
|
const TRANSPARENT = RGBA.fromValues(0, 0, 0, 0);
|
|
@@ -87,7 +93,7 @@ export class TerminalPaneRenderable extends Renderable {
|
|
|
87
93
|
cols,
|
|
88
94
|
rows,
|
|
89
95
|
cwd: this.opts.cwd ?? process.cwd(),
|
|
90
|
-
env:
|
|
96
|
+
env: this.opts.env ?? terminalEnvironment(process.env),
|
|
91
97
|
});
|
|
92
98
|
this.pty.onData((data) => {
|
|
93
99
|
// the shim streams a UTF-8-decoded string (split multibyte is handled); a
|