@cueloop/client 0.1.0-alpha.8 → 0.1.0-alpha.80
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/native/build-pty.sh +51 -0
- package/native/build.sh +82 -0
- package/native/darwin-arm64/libcuelooppty.dylib +0 -0
- package/native/darwin-arm64/libcueloopvt.dylib +0 -0
- package/native/src/pty.zig +193 -0
- package/native/src/shim.zig +114 -0
- package/package.json +40 -32
- package/src/App.appearance.test.tsx +53 -0
- package/src/App.autoclose.test.tsx +77 -51
- package/src/App.diff.test.tsx +176 -45
- package/src/App.export.test.tsx +58 -41
- package/src/App.focus.test.ts +89 -0
- package/src/App.identity.test.tsx +78 -0
- package/src/App.plan-render.test.tsx +209 -0
- package/src/App.plan-review.test.tsx +428 -0
- package/src/App.prototype-markdown.test.tsx +92 -0
- package/src/App.prototype.test.tsx +176 -0
- package/src/App.readonly.test.tsx +129 -67
- package/src/App.regression.test.tsx +94 -0
- package/src/App.rendering.test.tsx +61 -35
- package/src/App.share-dialog.test.tsx +172 -0
- package/src/App.submit-confirm.test.tsx +222 -0
- package/src/App.test.tsx +333 -131
- package/src/App.toast.test.tsx +98 -0
- package/src/App.tsx +1338 -608
- package/src/App.visual.test.tsx +57 -0
- package/src/App.walk.test.tsx +299 -0
- package/src/App.workbench.test.tsx +701 -0
- package/src/__snapshots__/view-plan.regression.test.ts.snap +100 -0
- package/src/annotation-palette.ts +62 -0
- package/src/ansi-screen.test.ts +28 -0
- package/src/ansi-screen.ts +72 -0
- package/src/app-key-state.ts +61 -0
- package/src/app-screens.tsx +574 -0
- package/src/app-view-model.ts +162 -0
- package/src/attribution.test.ts +31 -0
- package/src/attribution.ts +42 -0
- package/src/clipboard.test.ts +23 -0
- package/src/clipboard.ts +33 -0
- package/src/components/AnnotatableFileView.stories.tsx +42 -0
- package/src/components/AnnotatableFileView.test.tsx +102 -0
- package/src/components/AnnotatableFileView.tsx +134 -0
- package/src/components/AnnotationCards.stories.tsx +86 -0
- package/src/components/AnnotationCards.tsx +341 -0
- package/src/components/AnsiScreen.stories.tsx +12 -0
- package/src/components/AnsiScreen.tsx +28 -0
- package/src/components/AppHeader.stories.tsx +50 -0
- package/src/components/AppHeader.tsx +83 -0
- package/src/components/AppShell.brand.test.tsx +58 -0
- package/src/components/AppShell.stories.tsx +89 -0
- package/src/components/AppShell.tsx +329 -0
- package/src/components/AppShell.zoom.test.tsx +135 -0
- package/src/components/BareWorkbenchFileView.stories.tsx +27 -0
- package/src/components/BareWorkbenchFileView.tsx +64 -0
- package/src/components/Breadcrumb.stories.tsx +19 -0
- package/src/components/Breadcrumb.tsx +45 -0
- package/src/components/ChangesColumn.stories.tsx +44 -0
- package/src/components/ChangesColumn.tsx +199 -0
- package/src/components/ChangesFileTree.test.tsx +61 -0
- package/src/components/CodeBlock.stories.tsx +42 -0
- package/src/components/CodeBlock.tsx +84 -0
- package/src/components/CompletionOverlay.stories.tsx +37 -0
- package/src/components/CompletionOverlay.tsx +77 -0
- package/src/components/ConfirmCard.stories.tsx +37 -0
- package/src/components/ConfirmCard.tsx +153 -0
- package/src/components/ConfirmDialog.stories.tsx +21 -0
- package/src/components/ConfirmDialog.tsx +72 -0
- package/src/components/DiffContentView.scroll.test.tsx +121 -0
- package/src/components/DiffContentView.split.test.tsx +140 -0
- package/src/components/DiffContentView.stories.tsx +80 -0
- package/src/components/DiffContentView.syntax.test.tsx +82 -0
- package/src/components/DiffContentView.tsx +876 -0
- package/src/components/DiscussionMarkerRail.stories.tsx +54 -0
- package/src/components/DiscussionMarkerRail.tsx +186 -0
- package/src/components/EditorGrid.stories.tsx +59 -0
- package/src/components/EditorGrid.tabs.test.tsx +118 -0
- package/src/components/EditorGrid.tsx +426 -0
- package/src/components/EditorGrid.zoom.test.tsx +68 -0
- package/src/components/FileContentsView.stories.tsx +16 -0
- package/src/components/FileContentsView.test.tsx +29 -0
- package/src/components/FileContentsView.tsx +98 -0
- package/src/components/GridTabContent.stories.tsx +59 -0
- package/src/components/GridTabContent.tsx +182 -0
- package/src/components/HandleChipsInput.stories.tsx +24 -0
- package/src/components/HandleChipsInput.test.tsx +35 -0
- package/src/components/HandleChipsInput.tsx +145 -0
- package/src/components/KeybindsDialog.stories.tsx +31 -0
- package/src/components/KeybindsDialog.tsx +103 -0
- package/src/components/MarkdownGridBlock.stories.tsx +33 -0
- package/src/components/MarkdownGridBlock.tsx +72 -0
- package/src/components/MarkdownThreadEditor.stories.tsx +15 -0
- package/src/components/MarkdownThreadEditor.test.tsx +98 -0
- package/src/components/MarkdownThreadEditor.tsx +162 -0
- package/src/components/MenuBar.stories.tsx +28 -0
- package/src/components/MenuBar.tsx +44 -0
- package/src/components/NavModeHint.stories.tsx +40 -0
- package/src/components/NavModeHint.tsx +31 -0
- package/src/components/OverlayScrollbar.stories.tsx +33 -0
- package/src/components/OverlayScrollbar.test.tsx +139 -0
- package/src/components/OverlayScrollbar.tsx +129 -0
- package/src/components/PaneGrid.stories.tsx +84 -0
- package/src/components/PaneGrid.tsx +128 -0
- package/src/components/PanelColumn.stories.tsx +25 -0
- package/src/components/PanelColumn.tsx +98 -0
- package/src/components/ProjectTreeView.stories.tsx +25 -0
- package/src/components/ProjectTreeView.test.tsx +59 -0
- package/src/components/ProjectTreeView.tsx +111 -0
- package/src/components/PromptDialog.stories.tsx +36 -0
- package/src/components/PromptDialog.test.tsx +61 -0
- package/src/components/PromptDialog.tsx +99 -0
- package/src/components/PrototypeContentView.stories.tsx +19 -0
- package/src/components/PrototypeContentView.tsx +446 -0
- package/src/components/RootOverlay.stories.tsx +45 -0
- package/src/components/RootOverlay.tsx +48 -0
- package/src/components/ScrollArea.stories.tsx +20 -0
- package/src/components/ScrollArea.tsx +44 -0
- package/src/components/SettingsDialog.stories.tsx +117 -0
- package/src/components/SettingsDialog.tsx +186 -0
- package/src/components/SettingsRows.stories.tsx +18 -0
- package/src/components/SettingsRows.tsx +112 -0
- package/src/components/ShareDialog.stories.tsx +121 -0
- package/src/components/ShareDialog.tsx +552 -0
- package/src/components/ShellHeader.stories.tsx +50 -0
- package/src/components/ShellHeader.tsx +137 -0
- package/src/components/SlashComposer.stories.tsx +37 -0
- package/src/components/SlashComposer.tsx +87 -0
- package/src/components/ThreadFooter.stories.tsx +30 -0
- package/src/components/ThreadFooter.test.tsx +47 -0
- package/src/components/ThreadFooter.tsx +76 -0
- package/src/components/ThreadTree.stories.tsx +46 -0
- package/src/components/ThreadTree.tsx +255 -0
- package/src/components/ThreadView.selection.test.tsx +899 -0
- package/src/components/ThreadView.slash.test.tsx +146 -0
- package/src/components/ThreadView.stories.tsx +26 -0
- package/src/components/ThreadView.tsx +472 -0
- package/src/components/Toast.stories.tsx +19 -0
- package/src/components/Toast.tsx +52 -0
- package/src/components/Tooltip.stories.tsx +26 -0
- package/src/components/Tooltip.tsx +72 -0
- package/src/components/TreePane.stories.tsx +106 -0
- package/src/components/TreePane.tsx +157 -0
- package/src/components/WalkWizard.stories.tsx +100 -0
- package/src/components/WalkWizard.tsx +150 -0
- package/src/components/WelcomePlayground.stories.tsx +12 -0
- package/src/components/WelcomePlayground.test.tsx +47 -0
- package/src/components/WelcomePlayground.tsx +142 -0
- package/src/components/__snapshots__/stories.test.tsx.snap +2238 -0
- package/src/components/diff-content-view-layout.test.ts +198 -0
- package/src/components/diff-content-view-layout.ts +84 -0
- package/src/components/editor-grid.test.ts +86 -0
- package/src/components/editor-grid.ts +138 -0
- package/src/components/file-tree.test.ts +60 -0
- package/src/components/file-tree.ts +112 -0
- package/src/components/menu-control.stories.tsx +35 -0
- package/src/components/menu-control.tsx +43 -0
- package/src/components/pages/Workspace.stories.tsx +181 -0
- package/src/components/primitives/Button.stories.tsx +32 -0
- package/src/components/primitives/Button.tsx +56 -0
- package/src/components/primitives/Card.stories.tsx +25 -0
- package/src/components/primitives/Card.tsx +63 -0
- package/src/components/primitives/Dialog.stories.tsx +24 -0
- package/src/components/primitives/Dialog.test.tsx +37 -0
- package/src/components/primitives/Dialog.tsx +71 -0
- package/src/components/primitives/DialogActions.stories.tsx +20 -0
- package/src/components/primitives/DialogActions.tsx +39 -0
- package/src/components/primitives/IconButton.stories.tsx +36 -0
- package/src/components/primitives/IconButton.tsx +62 -0
- package/src/components/primitives/StatusBar.stories.tsx +11 -0
- package/src/components/primitives/StatusBar.tsx +23 -0
- package/src/components/primitives/Switch.stories.tsx +17 -0
- package/src/components/primitives/Switch.tsx +32 -0
- package/src/components/primitives/Tabs.stories.tsx +29 -0
- package/src/components/primitives/Tabs.tsx +102 -0
- package/src/components/primitives/Toolbar.stories.tsx +17 -0
- package/src/components/primitives/Toolbar.tsx +14 -0
- package/src/components/primitives/Tree.stories.tsx +124 -0
- package/src/components/primitives/Tree.tsx +100 -0
- package/src/components/primitives/frame.test.ts +12 -0
- package/src/components/primitives/frame.ts +17 -0
- package/src/components/primitives/icons.ts +61 -0
- package/src/components/primitives/tree-model.test.ts +99 -0
- package/src/components/primitives/tree-model.ts +137 -0
- package/src/components/quick-actions-editor.stories.tsx +58 -0
- package/src/components/quick-actions-editor.test.tsx +117 -0
- package/src/components/quick-actions-editor.tsx +226 -0
- package/src/components/share-dialog-store.ts +58 -0
- package/src/components/stories-app.tsx +193 -0
- package/src/components/stories.test.tsx +74 -0
- package/src/components/story-fixtures.ts +102 -0
- package/src/components/story-tree.test.ts +108 -0
- package/src/components/story.ts +159 -0
- package/src/components/syntax-highlight.ts +133 -0
- package/src/components/terminal-pane.test.tsx +40 -0
- package/src/components/terminal-pane.ts +199 -0
- package/src/components/theme-context.stories.tsx +46 -0
- package/src/components/theme-context.tsx +37 -0
- package/src/components/thread-tree.test.ts +134 -0
- package/src/components/thread-tree.ts +109 -0
- package/src/components/truncate-text.ts +7 -0
- package/src/components/truncate-title.test.ts +31 -0
- package/src/components/truncate-title.ts +15 -0
- package/src/config-identity.test.ts +78 -0
- package/src/config.test.ts +517 -33
- package/src/config.ts +467 -57
- package/src/diff-fold.test.ts +78 -0
- package/src/diff-fold.ts +126 -0
- package/src/diff-hunk-curate.test.ts +315 -0
- package/src/diff-hunk-curate.ts +153 -0
- package/src/diff-intraline.test.ts +238 -0
- package/src/diff-intraline.ts +236 -0
- package/src/diff-syntax.test.ts +112 -0
- package/src/diff-syntax.ts +179 -0
- package/src/discussions.ts +79 -0
- package/src/editor.test.ts +149 -0
- package/src/editor.ts +139 -9
- package/src/ghostty-terminal.test.ts +85 -0
- package/src/ghostty-terminal.ts +199 -0
- package/src/github-identity.test.ts +58 -0
- package/src/github-identity.ts +41 -0
- package/src/herdr.test.ts +9 -34
- package/src/herdr.ts +25 -21
- package/src/index.ts +13 -0
- package/src/integrations.test.ts +41 -0
- package/src/integrations.ts +32 -0
- package/src/intent-dispatch.test.ts +723 -0
- package/src/intent-dispatch.ts +692 -0
- package/src/key-bindings.test.ts +156 -0
- package/src/key-bindings.ts +459 -0
- package/src/keymap.test.ts +461 -113
- package/src/keymap.ts +321 -79
- package/src/kitty-image.test.ts +66 -0
- package/src/kitty-image.ts +138 -0
- package/src/launch-layout.test.ts +52 -0
- package/src/launch-layout.ts +42 -0
- package/src/mark-runs.ts +121 -0
- package/src/markdown-editor-syntax-style.ts +50 -0
- package/src/markdown-grid.test.ts +101 -0
- package/src/markdown-grid.ts +203 -0
- package/src/markdown-highlight.test.ts +61 -0
- package/src/markdown-highlight.ts +122 -0
- package/src/pasted-image.test.ts +67 -0
- package/src/pasted-image.ts +60 -0
- package/src/perf/otlp-trace.test.ts +116 -0
- package/src/perf/otlp-trace.ts +130 -0
- package/src/perf/perf-report.ts +17 -0
- package/src/perf/perf-timings.test.ts +18 -0
- package/src/perf/perf-timings.ts +38 -0
- package/src/prototype-browser.test.ts +64 -0
- package/src/prototype-browser.ts +329 -0
- package/src/prototype-pixels.tsx +35 -0
- package/src/pty.test.ts +81 -0
- package/src/pty.ts +245 -0
- package/src/ready-signal.test.ts +85 -0
- package/src/ready-signal.ts +52 -0
- package/src/run.ts +78 -13
- package/src/serve.test.ts +24 -12
- package/src/serve.ts +52 -10
- package/src/share.ts +237 -0
- package/src/skills.test.ts +74 -0
- package/src/skills.ts +75 -0
- package/src/slash-palette.test.ts +103 -0
- package/src/slash-palette.ts +129 -0
- package/src/split-diff.test.ts +110 -0
- package/src/split-diff.ts +149 -0
- package/src/test-support.test.ts +264 -0
- package/src/test-support.ts +369 -0
- package/src/theme-presets.test.ts +51 -0
- package/src/theme-presets.ts +214 -0
- package/src/theme.ts +84 -19
- package/src/thread-chords.test.ts +79 -0
- package/src/thread-chords.ts +163 -0
- package/src/thread-controller.curate.test.ts +450 -0
- package/src/thread-controller.derivation.test.ts +62 -0
- package/src/thread-controller.live-diff.test.ts +322 -0
- package/src/thread-controller.optimistic.test.ts +69 -0
- package/src/thread-controller.share.test.ts +561 -0
- package/src/thread-controller.switch-cache.test.ts +176 -0
- package/src/thread-controller.tree.test.ts +261 -0
- package/src/thread-controller.ts +1893 -0
- package/src/thread-selection.test.ts +210 -0
- package/src/thread-selection.ts +243 -0
- package/src/tree-view.test.ts +128 -0
- package/src/tree-view.ts +148 -0
- package/src/use-annotation-surface.tsx +915 -0
- package/src/use-changes-workbench.ts +190 -0
- package/src/use-frame-measure.ts +41 -0
- package/src/use-remember-layout.ts +31 -0
- package/src/use-settings-dialog.tsx +320 -0
- package/src/use-terminal-virtualizer.ts +233 -0
- package/src/use-thread-body-editing.tsx +58 -0
- package/src/version.ts +5 -0
- package/src/view-diff.test.ts +137 -9
- package/src/view-diff.ts +269 -23
- package/src/view-plan.regression.test.ts +60 -0
- package/src/view-plan.test.ts +456 -0
- package/src/view-plan.ts +719 -0
- package/src/visual/contrast.test.ts +35 -0
- package/src/visual/contrast.ts +33 -0
- package/src/walk.test.ts +121 -0
- package/src/walk.ts +74 -0
- package/src/workbench-snapshot.test.ts +81 -0
- package/src/workbench-snapshot.ts +29 -0
- package/src/session-controller.ts +0 -345
- package/src/syntax.test.ts +0 -41
- package/src/syntax.ts +0 -88
- package/src/view.test.ts +0 -157
- package/src/view.ts +0 -312
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import { DARK } from "../theme";
|
|
5
|
+
import { press, settle } from "../test-support";
|
|
6
|
+
import { MarkdownThreadEditor, type MarkdownEditorHandle } from "./MarkdownThreadEditor";
|
|
7
|
+
|
|
8
|
+
describe("MarkdownThreadEditor", () => {
|
|
9
|
+
test("opens on the working copy and shows the caret position and save hint", async () => {
|
|
10
|
+
const setup = await testRender(
|
|
11
|
+
<MarkdownThreadEditor
|
|
12
|
+
initialText={"# Title\n\nbody text here\n"}
|
|
13
|
+
theme={DARK}
|
|
14
|
+
onExitEditor={() => {}}
|
|
15
|
+
/>,
|
|
16
|
+
{ width: 50, height: 12 },
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
await settle(setup);
|
|
20
|
+
const frame = setup.captureCharFrame();
|
|
21
|
+
|
|
22
|
+
expect(frame).toContain("Title");
|
|
23
|
+
expect(frame).toContain("body text here");
|
|
24
|
+
expect(frame).toContain("Ln 1/4");
|
|
25
|
+
expect(frame).toContain("save & close");
|
|
26
|
+
setup.renderer.destroy();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("the exit handle saves the current text and leaves (the header toggle's path)", async () => {
|
|
30
|
+
const handleRef = React.createRef<MarkdownEditorHandle>();
|
|
31
|
+
let exitedWith = "";
|
|
32
|
+
const setup = await testRender(
|
|
33
|
+
<MarkdownThreadEditor
|
|
34
|
+
ref={handleRef}
|
|
35
|
+
initialText={"keep this"}
|
|
36
|
+
theme={DARK}
|
|
37
|
+
onExitEditor={(text) => {
|
|
38
|
+
exitedWith = text;
|
|
39
|
+
}}
|
|
40
|
+
/>,
|
|
41
|
+
{ width: 40, height: 8 },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
await settle(setup);
|
|
45
|
+
handleRef.current?.requestExit();
|
|
46
|
+
|
|
47
|
+
expect(exitedWith).toBe("keep this");
|
|
48
|
+
setup.renderer.destroy();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("highlights align to the exact characters across lines (no newline drift)", async () => {
|
|
52
|
+
const setup = await testRender(
|
|
53
|
+
<MarkdownThreadEditor
|
|
54
|
+
initialText={"# Head\n\nrun `code` now\n"}
|
|
55
|
+
theme={DARK}
|
|
56
|
+
onExitEditor={() => {}}
|
|
57
|
+
/>,
|
|
58
|
+
{ width: 40, height: 10 },
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
await settle(setup);
|
|
62
|
+
const spans = setup.captureSpans().lines.flatMap((line) => line.spans);
|
|
63
|
+
const hexOf = (color: { toInts: () => number[] }): string =>
|
|
64
|
+
"#" +
|
|
65
|
+
color
|
|
66
|
+
.toInts()
|
|
67
|
+
.slice(0, 3)
|
|
68
|
+
.map((part) => part.toString(16).padStart(2, "0"))
|
|
69
|
+
.join("");
|
|
70
|
+
// the code highlight must cover exactly `code`, not a drifted slice like "ode` "
|
|
71
|
+
const codeSpan = spans.find((span) => span.text === "`code`");
|
|
72
|
+
|
|
73
|
+
expect(codeSpan).toBeDefined();
|
|
74
|
+
expect(codeSpan?.fg && hexOf(codeSpan.fg)).toBe(DARK.textDim);
|
|
75
|
+
setup.renderer.destroy();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("escape is a no-op - it never leaves the editor (IDE convention)", async () => {
|
|
79
|
+
let exits = 0;
|
|
80
|
+
const setup = await testRender(
|
|
81
|
+
<MarkdownThreadEditor
|
|
82
|
+
initialText={"stay put"}
|
|
83
|
+
theme={DARK}
|
|
84
|
+
onExitEditor={() => {
|
|
85
|
+
exits += 1;
|
|
86
|
+
}}
|
|
87
|
+
/>,
|
|
88
|
+
{ width: 40, height: 8 },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await settle(setup);
|
|
92
|
+
await press(setup, "escape");
|
|
93
|
+
|
|
94
|
+
expect(exits).toBe(0);
|
|
95
|
+
expect(setup.captureCharFrame()).toContain("save & close");
|
|
96
|
+
setup.renderer.destroy();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native inline markdown editor for a thread's body: a full-height text area
|
|
3
|
+
* over the working copy that lightly marks markdown as you type - a heading's
|
|
4
|
+
* marker dims and its title bolds, links color, inline and fenced code gray -
|
|
5
|
+
* and reports the caret line and column, replacing the external-editor hand-off so
|
|
6
|
+
* editing needs no per-user editor configuration. cmd, meta, or ctrl + enter
|
|
7
|
+
* saves the working copy and closes (loss-free, like the hand-off it replaces),
|
|
8
|
+
* as does the header edit/normal toggle; plain enter breaks the line. Escape is
|
|
9
|
+
* a no-op inside the editor, matching a modeless IDE - you leave by an explicit
|
|
10
|
+
* action, not by escaping the text. The raw markdown stays visible - a concealed
|
|
11
|
+
* rendered preview is a later layer.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { forwardRef, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
15
|
+
import type { KeyBinding, TextareaRenderable } from "@opentui/core";
|
|
16
|
+
import { type Theme } from "../theme";
|
|
17
|
+
import { markdownHighlightRanges } from "../markdown-highlight";
|
|
18
|
+
import { markdownEditorStyle } from "../markdown-editor-syntax-style";
|
|
19
|
+
|
|
20
|
+
export interface MarkdownThreadEditorProps {
|
|
21
|
+
/** The thread body's working copy; the editor opens on this markdown text. */
|
|
22
|
+
initialText: string;
|
|
23
|
+
theme: Theme;
|
|
24
|
+
/** Called with the edited markdown when the reviewer leaves (cmd/ctrl+enter or the header toggle); both save. */
|
|
25
|
+
onExitEditor: (text: string) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** An imperative exit for callers outside the editor (the header edit/normal toggle): save the current text and leave. */
|
|
29
|
+
export interface MarkdownEditorHandle {
|
|
30
|
+
requestExit: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// three modifiers cover terminal variance for save-and-close; plain enter is a newline, escape stays unbound
|
|
34
|
+
const MARKDOWN_EDITOR_KEY_BINDINGS: KeyBinding[] = [
|
|
35
|
+
{ name: "return", super: true, action: "submit" },
|
|
36
|
+
{ name: "return", meta: true, action: "submit" },
|
|
37
|
+
{ name: "return", ctrl: true, action: "submit" },
|
|
38
|
+
{ name: "a", super: true, action: "select-all" },
|
|
39
|
+
{ name: "a", meta: true, action: "select-all" },
|
|
40
|
+
{ name: "return", action: "newline" },
|
|
41
|
+
{ name: "return", shift: true, action: "newline" },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** The caret's 1-based line and column, plus the buffer's total line count, for the status footer. */
|
|
45
|
+
interface MarkdownEditorPosition {
|
|
46
|
+
line: number;
|
|
47
|
+
column: number;
|
|
48
|
+
lineCount: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** For each offset 0..length, the count of newlines before it, mapping a source offset to the buffer's newline-excluded offset. */
|
|
52
|
+
function newlinePrefixCounts(text: string): number[] {
|
|
53
|
+
const counts = Array.from({ length: text.length + 1 }, () => 0);
|
|
54
|
+
let seen = 0;
|
|
55
|
+
|
|
56
|
+
for (let index = 0; index < text.length; index++) {
|
|
57
|
+
counts[index] = seen;
|
|
58
|
+
if (text[index] === "\n") seen += 1;
|
|
59
|
+
}
|
|
60
|
+
counts[text.length] = seen;
|
|
61
|
+
|
|
62
|
+
return counts;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const MarkdownThreadEditor = forwardRef<MarkdownEditorHandle, MarkdownThreadEditorProps>(
|
|
66
|
+
function MarkdownThreadEditor({ initialText, theme, onExitEditor }, handleRef): React.ReactNode {
|
|
67
|
+
const editorRef = useRef<TextareaRenderable | null>(null);
|
|
68
|
+
const [position, setPosition] = useState<MarkdownEditorPosition>({
|
|
69
|
+
line: 1,
|
|
70
|
+
column: 1,
|
|
71
|
+
lineCount: initialText.split("\n").length,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const exitWithCurrentText = (): void =>
|
|
75
|
+
onExitEditor(editorRef.current?.plainText ?? initialText);
|
|
76
|
+
|
|
77
|
+
useImperativeHandle(handleRef, () => ({ requestExit: exitWithCurrentText }));
|
|
78
|
+
|
|
79
|
+
const paintMarkdown = (editor: TextareaRenderable): void => {
|
|
80
|
+
const { styleIdFor } = markdownEditorStyle(theme);
|
|
81
|
+
const text = editor.plainText;
|
|
82
|
+
// addHighlightByCharRange counts offsets with newlines excluded, so map source offsets down by the
|
|
83
|
+
// newlines before them - otherwise a highlight drifts one cell per preceding line
|
|
84
|
+
const newlinesBefore = newlinePrefixCounts(text);
|
|
85
|
+
|
|
86
|
+
editor.editBuffer.clearAllHighlights();
|
|
87
|
+
for (const range of markdownHighlightRanges(text)) {
|
|
88
|
+
editor.editBuffer.addHighlightByCharRange({
|
|
89
|
+
start: range.start - newlinesBefore[range.start]!,
|
|
90
|
+
end: range.end - newlinesBefore[range.end]!,
|
|
91
|
+
styleId: styleIdFor(range.group),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// paint synchronously at commit so the first frame already shows styled markdown
|
|
97
|
+
useLayoutEffect(() => {
|
|
98
|
+
const editor = editorRef.current;
|
|
99
|
+
|
|
100
|
+
if (!editor) return;
|
|
101
|
+
editor.editBuffer.setSyntaxStyle(markdownEditorStyle(theme).style);
|
|
102
|
+
paintMarkdown(editor);
|
|
103
|
+
setPosition((prior) => ({ ...prior, lineCount: editor.editBuffer.getLineCount() }));
|
|
104
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
105
|
+
}, []);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<box style={{ flexDirection: "column", flexGrow: 1, paddingLeft: 2 }}>
|
|
109
|
+
<textarea
|
|
110
|
+
ref={editorRef}
|
|
111
|
+
focused
|
|
112
|
+
initialValue={initialText}
|
|
113
|
+
keyBindings={MARKDOWN_EDITOR_KEY_BINDINGS}
|
|
114
|
+
wrapMode="word"
|
|
115
|
+
cursorStyle={{ style: "block", blinking: true }}
|
|
116
|
+
onSubmit={exitWithCurrentText}
|
|
117
|
+
onCursorChange={(event) =>
|
|
118
|
+
setPosition((prior) => ({
|
|
119
|
+
...prior,
|
|
120
|
+
line: event.line + 1,
|
|
121
|
+
column: event.visualColumn + 1,
|
|
122
|
+
}))
|
|
123
|
+
}
|
|
124
|
+
onContentChange={() => {
|
|
125
|
+
const editor = editorRef.current;
|
|
126
|
+
|
|
127
|
+
if (!editor) return;
|
|
128
|
+
paintMarkdown(editor);
|
|
129
|
+
setPosition((prior) => ({ ...prior, lineCount: editor.editBuffer.getLineCount() }));
|
|
130
|
+
}}
|
|
131
|
+
style={{
|
|
132
|
+
flexGrow: 1,
|
|
133
|
+
backgroundColor: "transparent",
|
|
134
|
+
focusedBackgroundColor: "transparent",
|
|
135
|
+
textColor: theme.text,
|
|
136
|
+
focusedTextColor: theme.text,
|
|
137
|
+
}}
|
|
138
|
+
/>
|
|
139
|
+
<MarkdownEditorStatus position={position} theme={theme} />
|
|
140
|
+
</box>
|
|
141
|
+
);
|
|
142
|
+
},
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/** The one-row footer under the editor: caret line and column, total lines, and the save-and-close hint. */
|
|
146
|
+
function MarkdownEditorStatus({
|
|
147
|
+
position,
|
|
148
|
+
theme,
|
|
149
|
+
}: {
|
|
150
|
+
position: MarkdownEditorPosition;
|
|
151
|
+
theme: Theme;
|
|
152
|
+
}): React.ReactNode {
|
|
153
|
+
return (
|
|
154
|
+
<box style={{ flexDirection: "row", flexShrink: 0 }}>
|
|
155
|
+
<text
|
|
156
|
+
fg={theme.textMuted}
|
|
157
|
+
>{`Ln ${position.line}/${position.lineCount} Col ${position.column}`}</text>
|
|
158
|
+
<box style={{ flexGrow: 1 }} />
|
|
159
|
+
<text fg={theme.textDim}>{"⌃⏎ save & close"}</text>
|
|
160
|
+
</box>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { MenuBar } from "./MenuBar";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Chrome/MenuBar" };
|
|
7
|
+
|
|
8
|
+
const callbacks = { onSettings: () => {}, onKeybinds: () => {} };
|
|
9
|
+
|
|
10
|
+
function BottomFrame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
11
|
+
return (
|
|
12
|
+
<box
|
|
13
|
+
style={{ flexDirection: "column", width: "100%", height: "100%", justifyContent: "flex-end" }}
|
|
14
|
+
>
|
|
15
|
+
{children}
|
|
16
|
+
</box>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const Open: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<BottomFrame>
|
|
23
|
+
<MenuBar open {...callbacks} />
|
|
24
|
+
</BottomFrame>
|
|
25
|
+
),
|
|
26
|
+
expectedColors: [DARK.text],
|
|
27
|
+
size: { width: 60, height: 8 },
|
|
28
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Theme } from "../theme";
|
|
3
|
+
import { useComponentTheme } from "./theme-context";
|
|
4
|
+
import { FRAME_BORDER_STYLE } from "./primitives/frame";
|
|
5
|
+
|
|
6
|
+
export interface MenuBarProps {
|
|
7
|
+
open: boolean;
|
|
8
|
+
onSettings: () => void;
|
|
9
|
+
onKeybinds: () => void;
|
|
10
|
+
theme?: Theme;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DROPDOWN_OPTION_ROWS = 2;
|
|
14
|
+
|
|
15
|
+
/** The settings gear's drop-down: Settings and Keybinds, below the top-left gear. */
|
|
16
|
+
export function MenuBar({ open, onSettings, onKeybinds, theme }: MenuBarProps): React.ReactNode {
|
|
17
|
+
const tokens = useComponentTheme(theme);
|
|
18
|
+
const dropdownHeight = DROPDOWN_OPTION_ROWS + 2;
|
|
19
|
+
|
|
20
|
+
if (!open) return null;
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<box
|
|
24
|
+
style={{
|
|
25
|
+
position: "absolute",
|
|
26
|
+
left: 1,
|
|
27
|
+
top: 2,
|
|
28
|
+
width: 16,
|
|
29
|
+
height: dropdownHeight,
|
|
30
|
+
border: true,
|
|
31
|
+
borderStyle: FRAME_BORDER_STYLE,
|
|
32
|
+
borderColor: tokens.text,
|
|
33
|
+
flexDirection: "column",
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
<box onMouseUp={onSettings}>
|
|
37
|
+
<text fg={tokens.text}> Settings</text>
|
|
38
|
+
</box>
|
|
39
|
+
<box onMouseUp={onKeybinds}>
|
|
40
|
+
<text fg={tokens.text}> Keybinds</text>
|
|
41
|
+
</box>
|
|
42
|
+
</box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { DARK } from "../theme";
|
|
3
|
+
import type { Story, StoryMeta } from "./story";
|
|
4
|
+
import { NavModeHint } from "./NavModeHint";
|
|
5
|
+
|
|
6
|
+
export const meta: StoryMeta = { title: "Chrome/NavModeHint" };
|
|
7
|
+
|
|
8
|
+
function Frame({ children }: { children: React.ReactNode }): React.ReactNode {
|
|
9
|
+
return <box style={{ width: "100%", backgroundColor: DARK.panel }}>{children}</box>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const ThreadNav: Story = {
|
|
13
|
+
render: () => (
|
|
14
|
+
<Frame>
|
|
15
|
+
<NavModeHint navMode surface="thread" theme={DARK} />
|
|
16
|
+
</Frame>
|
|
17
|
+
),
|
|
18
|
+
expectedColors: [DARK.textDim],
|
|
19
|
+
size: { width: 80, height: 1 },
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export const DiffNav: Story = {
|
|
23
|
+
render: () => (
|
|
24
|
+
<Frame>
|
|
25
|
+
<NavModeHint navMode surface="diff" theme={DARK} />
|
|
26
|
+
</Frame>
|
|
27
|
+
),
|
|
28
|
+
expectedColors: [DARK.textDim],
|
|
29
|
+
size: { width: 80, height: 1 },
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Composing: Story = {
|
|
33
|
+
render: () => (
|
|
34
|
+
<Frame>
|
|
35
|
+
<NavModeHint navMode={false} surface="thread" theme={DARK} />
|
|
36
|
+
</Frame>
|
|
37
|
+
),
|
|
38
|
+
expectedColors: [DARK.textDim],
|
|
39
|
+
size: { width: 80, height: 1 },
|
|
40
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { Theme } from "../theme";
|
|
3
|
+
|
|
4
|
+
const DIFF_NAV_HINT = "x reject · c fold · d layout · k walk · ⏎ submit · type to leave";
|
|
5
|
+
const THREAD_NAV_HINT = "c comment · e edit · s share · n/p cards · ⏎ submit · type to leave";
|
|
6
|
+
const BARE_NAV_HINT = "c comment · z fold · type to leave";
|
|
7
|
+
const COMPOSE_HINT = "type to comment · esc for nav mode";
|
|
8
|
+
|
|
9
|
+
const NAV_HINTS = { thread: THREAD_NAV_HINT, diff: DIFF_NAV_HINT, bare: BARE_NAV_HINT };
|
|
10
|
+
|
|
11
|
+
export type NavHintSurface = keyof typeof NAV_HINTS;
|
|
12
|
+
|
|
13
|
+
export function NavModeHint({
|
|
14
|
+
navMode,
|
|
15
|
+
surface,
|
|
16
|
+
theme,
|
|
17
|
+
}: {
|
|
18
|
+
navMode: boolean;
|
|
19
|
+
surface: NavHintSurface;
|
|
20
|
+
theme: Theme;
|
|
21
|
+
}): React.ReactNode {
|
|
22
|
+
const navHint = NAV_HINTS[surface];
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<box style={{ height: 1, flexDirection: "row", paddingLeft: 2 }}>
|
|
26
|
+
<text selectable={false} fg={theme.textDim}>
|
|
27
|
+
{navMode ? navHint : COMPOSE_HINT}
|
|
28
|
+
</text>
|
|
29
|
+
</box>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useRef } from "react";
|
|
2
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
+
import { DARK } from "../theme";
|
|
4
|
+
import { OverlayScrollbar } from "./OverlayScrollbar";
|
|
5
|
+
import type { Story, StoryMeta } from "./story";
|
|
6
|
+
|
|
7
|
+
export const meta: StoryMeta = { title: "Chrome/OverlayScrollbar" };
|
|
8
|
+
|
|
9
|
+
function TallSurface(): React.ReactNode {
|
|
10
|
+
const scrollRef = useRef<ScrollBoxRenderable | null>(null);
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<box style={{ width: 24, height: 8, flexDirection: "row", backgroundColor: DARK.background }}>
|
|
14
|
+
<scrollbox
|
|
15
|
+
ref={scrollRef}
|
|
16
|
+
style={{ flexGrow: 1 }}
|
|
17
|
+
focused={false}
|
|
18
|
+
verticalScrollbarOptions={{ visible: false }}
|
|
19
|
+
>
|
|
20
|
+
{Array.from({ length: 24 }, (_unused, index) => (
|
|
21
|
+
<text key={index} fg={DARK.textMuted}>{`row ${index + 1}`}</text>
|
|
22
|
+
))}
|
|
23
|
+
</scrollbox>
|
|
24
|
+
<OverlayScrollbar scrollbox={scrollRef} />
|
|
25
|
+
</box>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// At rest the overlay bar is hidden; the thumb only appears while the surface is scrolling.
|
|
30
|
+
export const AtRest: Story = {
|
|
31
|
+
render: () => <TallSurface />,
|
|
32
|
+
size: { width: 24, height: 8 },
|
|
33
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The mirrored scrollbar: thumb geometry follows the scroll state, and on a surface it
|
|
3
|
+
* draws as the panel's rightmost column, past the discussion dots - not inside the
|
|
4
|
+
* scrollbox's own viewport row.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
9
|
+
import { ScrollBoxRenderable, type Renderable } from "@opentui/core";
|
|
10
|
+
import React from "react";
|
|
11
|
+
import { DiffContentView } from "./DiffContentView";
|
|
12
|
+
import { diffRows, marksByRows } from "../view-diff";
|
|
13
|
+
import { allowEventLoopUpdates } from "../test-support";
|
|
14
|
+
import { fixtureDiffSession } from "./story-fixtures";
|
|
15
|
+
import { DARK } from "../theme";
|
|
16
|
+
import { thumbGeometry } from "./OverlayScrollbar";
|
|
17
|
+
|
|
18
|
+
const noop = (): void => {};
|
|
19
|
+
|
|
20
|
+
function findById(node: Renderable, id: string): Renderable | undefined {
|
|
21
|
+
if (node.id === id) return node;
|
|
22
|
+
for (const child of node.getChildren()) {
|
|
23
|
+
const found = findById(child, id);
|
|
24
|
+
|
|
25
|
+
if (found) return found;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hex(color: { toInts(): [number, number, number, number] }): string {
|
|
32
|
+
const [red, green, blue] = color.toInts();
|
|
33
|
+
|
|
34
|
+
return "#" + [red, green, blue].map((part) => part.toString(16).padStart(2, "0")).join("");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("thumbGeometry", () => {
|
|
38
|
+
test("nothing to scroll draws no thumb", () => {
|
|
39
|
+
// Arrange / Act / Assert - content fits the viewport
|
|
40
|
+
expect(thumbGeometry({ top: 0, height: 10, viewport: 14 }, 14)).toBeNull();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("the thumb's height is the viewport's share of the content, at least one row", () => {
|
|
44
|
+
// Act
|
|
45
|
+
const tall = thumbGeometry({ top: 0, height: 44, viewport: 14 }, 14);
|
|
46
|
+
const tiny = thumbGeometry({ top: 0, height: 10_000, viewport: 14 }, 14);
|
|
47
|
+
|
|
48
|
+
// Assert
|
|
49
|
+
expect(tall).toEqual({ row: 0, height: 4 });
|
|
50
|
+
expect(tiny?.height).toBe(1);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("the thumb reaches the track's end at the maximum scroll offset", () => {
|
|
54
|
+
// Act - scrolled to the bottom
|
|
55
|
+
const thumb = thumbGeometry({ top: 30, height: 44, viewport: 14 }, 14);
|
|
56
|
+
|
|
57
|
+
// Assert - its last row is the track's last row
|
|
58
|
+
expect(thumb).toEqual({ row: 10, height: 4 });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe("the diff sheet's scrollbar", () => {
|
|
63
|
+
const body = Array.from({ length: 40 }, (_unused, index) => `+line ${index} of the change;`).join(
|
|
64
|
+
"\n",
|
|
65
|
+
);
|
|
66
|
+
const patch = `diff --git a/x.ts b/x.ts\n--- a/x.ts\n+++ b/x.ts\n@@ -0,0 +1,40 @@\n${body}\n`;
|
|
67
|
+
|
|
68
|
+
test("draws as the rightmost column, past the discussion dot", async () => {
|
|
69
|
+
// Arrange - a diff taller than the viewport, with one discussion so the dot rail has a dot
|
|
70
|
+
const rows = diffRows(patch);
|
|
71
|
+
const session = fixtureDiffSession({
|
|
72
|
+
annotations: [
|
|
73
|
+
{
|
|
74
|
+
id: "a1",
|
|
75
|
+
kind: "comment",
|
|
76
|
+
body: "hm",
|
|
77
|
+
anchor: { quote: "line 7 of the change;", prefix: "", suffix: "" },
|
|
78
|
+
createdAt: "2026-01-01T00:00:00Z",
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
const width = 60;
|
|
83
|
+
const setup = await testRender(
|
|
84
|
+
<DiffContentView
|
|
85
|
+
rows={rows}
|
|
86
|
+
session={session}
|
|
87
|
+
marks={marksByRows(session.annotations, rows)}
|
|
88
|
+
quickActions={[]}
|
|
89
|
+
observer={false}
|
|
90
|
+
onAnnotate={noop}
|
|
91
|
+
onReply={noop}
|
|
92
|
+
onUpdateAnnotation={noop}
|
|
93
|
+
onExit={noop}
|
|
94
|
+
/>,
|
|
95
|
+
{ width, height: 14 },
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
allowEventLoopUpdates();
|
|
99
|
+
await setup.waitForVisualIdle();
|
|
100
|
+
// the overlay bar hides at rest; a scroll reveals the thumb
|
|
101
|
+
const scroller = findById(setup.renderer.root, "diff-scroll");
|
|
102
|
+
|
|
103
|
+
if (!(scroller instanceof ScrollBoxRenderable))
|
|
104
|
+
throw new Error("diff-scroll is not a scrollbox");
|
|
105
|
+
const countThumbRows = (): number =>
|
|
106
|
+
setup.captureSpans().lines.filter((line) => {
|
|
107
|
+
let column = 0;
|
|
108
|
+
|
|
109
|
+
for (const span of line.spans) {
|
|
110
|
+
column += span.text.length;
|
|
111
|
+
if (column >= width) return span.bg !== undefined && hex(span.bg) === DARK.textDim;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return false;
|
|
115
|
+
}).length;
|
|
116
|
+
|
|
117
|
+
// the overlay bar reveals on a scroll and hides after an idle delay; alternate the offset to
|
|
118
|
+
// re-trigger the reveal until the thumb is captured, so a slow CI settle never races the hide
|
|
119
|
+
let thumbRows = 0;
|
|
120
|
+
|
|
121
|
+
for (let attempt = 0; attempt < 20 && thumbRows === 0; attempt++) {
|
|
122
|
+
scroller.scrollTo(attempt % 2 === 0 ? 4 : 5);
|
|
123
|
+
await setup.waitForVisualIdle();
|
|
124
|
+
thumbRows = countThumbRows();
|
|
125
|
+
}
|
|
126
|
+
const frame = setup.captureCharFrame().split("\n");
|
|
127
|
+
|
|
128
|
+
// Assert - the thumb occupies the last column at the top (scroll offset 0), and the dot sits
|
|
129
|
+
// to its left in the rail, so the order reads content, dots, scrollbar
|
|
130
|
+
expect(thumbRows).toBe(4);
|
|
131
|
+
const dotLine = frame.find((line) => line.includes("○"));
|
|
132
|
+
|
|
133
|
+
expect(dotLine).toBeDefined();
|
|
134
|
+
expect(dotLine!.indexOf("○")).toBeLessThan(width - 1);
|
|
135
|
+
// let the async highlight settle before the renderer goes away
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
137
|
+
setup.renderer.destroy();
|
|
138
|
+
});
|
|
139
|
+
});
|