@selesai/code 0.9.15 → 0.10.1
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/CHANGELOG.md +27 -0
- package/README.md +6 -9
- package/dist/defaults/models.json +2 -2
- package/dist/defaults/settings.json +3 -7
- package/dist/extensions/package.json +2 -4
- package/dist/extensions/pi-zentui/.editorconfig +12 -0
- package/dist/extensions/pi-zentui/.github/CODEOWNERS +1 -0
- package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/bug_report.yml +110 -0
- package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/config_question.yml +56 -0
- package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/feature_request.yml +63 -0
- package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/rendering_issue.yml +106 -0
- package/dist/extensions/pi-zentui/.github/ISSUE_TEMPLATE/runtime_detection.yml +103 -0
- package/dist/extensions/pi-zentui/.github/dependabot.yml +47 -0
- package/dist/extensions/pi-zentui/.github/pull_request_template.md +34 -0
- package/dist/extensions/pi-zentui/.github/release.yml +18 -0
- package/dist/extensions/pi-zentui/.github/workflows/ci.yml +79 -0
- package/dist/extensions/pi-zentui/.github/workflows/publish.yml +63 -0
- package/dist/extensions/pi-zentui/AGENTS.md +107 -0
- package/dist/extensions/pi-zentui/CONTRIBUTING.md +68 -0
- package/dist/extensions/pi-zentui/LICENSE +21 -0
- package/dist/extensions/pi-zentui/README.md +188 -0
- package/dist/extensions/pi-zentui/SECURITY.md +18 -0
- package/dist/extensions/pi-zentui/assets/main-cover.png +0 -0
- package/dist/extensions/pi-zentui/assets/screenshots/editor-accent-rail.png +0 -0
- package/dist/extensions/pi-zentui/assets/screenshots/editor-minimalist.png +0 -0
- package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode-copy-friendly.png +0 -0
- package/dist/extensions/pi-zentui/assets/screenshots/editor-opencode.png +0 -0
- package/dist/extensions/pi-zentui/assets/screenshots/user-message-compact.png +0 -0
- package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed-copy-friendly.png +0 -0
- package/dist/extensions/pi-zentui/assets/screenshots/user-message-framed.png +0 -0
- package/dist/extensions/pi-zentui/assets/screenshots/user-message-labeled.png +0 -0
- package/dist/extensions/pi-zentui/assets/zentui.png +0 -0
- package/dist/extensions/pi-zentui/biome.json +31 -0
- package/dist/extensions/pi-zentui/docs/configuration.md +509 -0
- package/dist/extensions/pi-zentui/docs/footer-format.md +162 -0
- package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-editor.ts +99 -0
- package/dist/extensions/pi-zentui/extensions/zentui/accent-rail-layout-patch.ts +457 -0
- package/dist/extensions/pi-zentui/extensions/zentui/completion-menu.ts +157 -0
- package/dist/extensions/pi-zentui/extensions/zentui/config.ts +2100 -0
- package/dist/extensions/pi-zentui/extensions/zentui/editor-metadata-format.ts +321 -0
- package/dist/extensions/pi-zentui/extensions/zentui/editor-transfer.ts +73 -0
- package/dist/extensions/pi-zentui/extensions/zentui/extension-status.ts +89 -0
- package/dist/extensions/pi-zentui/extensions/zentui/footer-format.ts +364 -0
- package/dist/extensions/pi-zentui/extensions/zentui/footer-layout.ts +122 -0
- package/dist/extensions/pi-zentui/extensions/zentui/footer.ts +812 -0
- package/dist/extensions/pi-zentui/extensions/zentui/format.ts +568 -0
- package/dist/extensions/pi-zentui/extensions/zentui/git.ts +384 -0
- package/dist/extensions/pi-zentui/extensions/zentui/icons.ts +252 -0
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +1406 -0
- package/dist/extensions/pi-zentui/extensions/zentui/interaction-summary.ts +1049 -0
- package/dist/extensions/pi-zentui/extensions/zentui/live-context.ts +75 -0
- package/dist/extensions/pi-zentui/extensions/zentui/minimalist-editor.ts +463 -0
- package/dist/extensions/pi-zentui/extensions/zentui/package-version.ts +651 -0
- package/dist/extensions/pi-zentui/extensions/zentui/project-refresh.ts +120 -0
- package/dist/extensions/pi-zentui/extensions/zentui/project-state.ts +59 -0
- package/dist/extensions/pi-zentui/extensions/zentui/prototype-patch-registry.ts +166 -0
- package/dist/extensions/pi-zentui/extensions/zentui/repository-root.ts +98 -0
- package/dist/extensions/pi-zentui/extensions/zentui/runtime.ts +841 -0
- package/dist/extensions/pi-zentui/extensions/zentui/selector-border.ts +93 -0
- package/dist/extensions/pi-zentui/extensions/zentui/session-lifecycle.ts +60 -0
- package/dist/extensions/pi-zentui/extensions/zentui/settings-command.ts +1796 -0
- package/dist/extensions/pi-zentui/extensions/zentui/settings-previews.ts +128 -0
- package/dist/extensions/pi-zentui/extensions/zentui/state.ts +94 -0
- package/dist/extensions/pi-zentui/extensions/zentui/style.ts +424 -0
- package/dist/extensions/pi-zentui/extensions/zentui/telemetry.ts +82 -0
- package/dist/extensions/pi-zentui/extensions/zentui/ui.ts +1290 -0
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-osc.ts +248 -0
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +215 -0
- package/dist/extensions/pi-zentui/extensions/zentui/user-message.ts +206 -0
- package/dist/extensions/pi-zentui/extensions/zentui/working-line-messages.ts +24 -0
- package/dist/extensions/pi-zentui/extensions/zentui/working-line-spinners.ts +54 -0
- package/dist/extensions/pi-zentui/extensions/zentui/working-line.ts +1433 -0
- package/dist/extensions/pi-zentui/package-lock.json +5311 -0
- package/dist/extensions/pi-zentui/package.json +60 -0
- package/dist/extensions/pi-zentui/test/accent-rail-editor.test.ts +232 -0
- package/dist/extensions/pi-zentui/test/accent-rail-layout-patch.test.ts +669 -0
- package/dist/extensions/pi-zentui/test/completion-menu.test.ts +265 -0
- package/dist/extensions/pi-zentui/test/config.test.ts +3119 -0
- package/dist/extensions/pi-zentui/test/editor-metadata-format.test.ts +405 -0
- package/dist/extensions/pi-zentui/test/editor-transfer.test.ts +260 -0
- package/dist/extensions/pi-zentui/test/editor-viewport-indicators.test.ts +1286 -0
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +7196 -0
- package/dist/extensions/pi-zentui/test/extension-status.test.ts +182 -0
- package/dist/extensions/pi-zentui/test/fixtures/starship-nerd-font-symbols.toml +172 -0
- package/dist/extensions/pi-zentui/test/footer-format.test.ts +431 -0
- package/dist/extensions/pi-zentui/test/footer-layout.test.ts +113 -0
- package/dist/extensions/pi-zentui/test/footer-repository-path.test.ts +123 -0
- package/dist/extensions/pi-zentui/test/format.test.ts +865 -0
- package/dist/extensions/pi-zentui/test/git.test.ts +212 -0
- package/dist/extensions/pi-zentui/test/icons.test.ts +69 -0
- package/dist/extensions/pi-zentui/test/interaction-summary.test.ts +1402 -0
- package/dist/extensions/pi-zentui/test/live-context-integration.test.ts +378 -0
- package/dist/extensions/pi-zentui/test/live-context.test.ts +139 -0
- package/dist/extensions/pi-zentui/test/minimalist-editor.test.ts +811 -0
- package/dist/extensions/pi-zentui/test/package-version.test.ts +321 -0
- package/dist/extensions/pi-zentui/test/project-refresh.test.ts +250 -0
- package/dist/extensions/pi-zentui/test/project-state.test.ts +159 -0
- package/dist/extensions/pi-zentui/test/repository-root.test.ts +108 -0
- package/dist/extensions/pi-zentui/test/responsive-dependencies.test.ts +825 -0
- package/dist/extensions/pi-zentui/test/runtime.test.ts +183 -0
- package/dist/extensions/pi-zentui/test/session-lifecycle.test.ts +40 -0
- package/dist/extensions/pi-zentui/test/settings-command.test.ts +1381 -0
- package/dist/extensions/pi-zentui/test/settings-previews.test.ts +347 -0
- package/dist/extensions/pi-zentui/test/state.test.ts +84 -0
- package/dist/extensions/pi-zentui/test/telemetry-lifecycle-integration.test.ts +276 -0
- package/dist/extensions/pi-zentui/test/telemetry.test.ts +105 -0
- package/dist/extensions/pi-zentui/test/turn-summary-lifecycle.integration.test.ts +327 -0
- package/dist/extensions/pi-zentui/test/user-message-osc.test.ts +164 -0
- package/dist/extensions/pi-zentui/test/user-message-styles.test.ts +292 -0
- package/dist/extensions/pi-zentui/test/working-line-lifecycle.integration.test.ts +730 -0
- package/dist/extensions/pi-zentui/test/working-line-messages.test.ts +34 -0
- package/dist/extensions/pi-zentui/test/working-line.test.ts +1990 -0
- package/dist/extensions/pi-zentui/tsconfig.json +16 -0
- package/dist/extensions/tps.test.ts +38 -0
- package/dist/extensions/{pi-powerline-footer/tps.ts → tps.ts} +4 -0
- package/dist/modes/interactive/components/settings-selector.d.ts +1 -1
- package/dist/modes/interactive/interactive-mode.js +1 -1
- package/dist/skills/design-references/SKILL.md +31 -0
- package/dist/skills/design-references/references/clickhouse/DESIGN.md +544 -0
- package/dist/themes/adventure.json +84 -0
- package/dist/themes/adwaita-dark.json +84 -0
- package/dist/themes/arcoiris.json +84 -0
- package/dist/themes/arthur.json +84 -0
- package/dist/themes/atom.json +84 -0
- package/dist/themes/aura.json +84 -0
- package/dist/themes/black-metal-bathory.json +84 -0
- package/dist/themes/black-metal-burzum.json +84 -0
- package/dist/themes/black-metal-khold.json +84 -0
- package/dist/themes/box.json +84 -0
- package/dist/themes/brogrammer.json +84 -0
- package/dist/themes/carbonfox.json +84 -0
- package/dist/themes/catppuccin-mocha.json +84 -0
- package/dist/themes/citruszest.json +84 -0
- package/dist/themes/cursor-dark.json +84 -0
- package/dist/themes/cutie-pro.json +84 -0
- package/dist/themes/dark-modern.json +84 -0
- package/dist/themes/dark-pastel.json +84 -0
- package/dist/themes/dimmed-monokai.json +84 -0
- package/dist/themes/doom-peacock.json +84 -0
- package/dist/themes/dracula-plus.json +84 -0
- package/dist/themes/dracula.json +80 -0
- package/dist/themes/earthsong.json +84 -0
- package/dist/themes/everforest-dark-hard.json +84 -0
- package/dist/themes/everforest-dark.json +80 -0
- package/dist/themes/fahrenheit.json +84 -0
- package/dist/themes/flatland.json +84 -0
- package/dist/themes/flexoki-dark.json +84 -0
- package/dist/themes/front-end-delight.json +84 -0
- package/dist/themes/fun-forrest.json +84 -0
- package/dist/themes/galizur.json +84 -0
- package/dist/themes/github-dark-colorblind.json +84 -0
- package/dist/themes/github-dark-high-contrast.json +84 -0
- package/dist/themes/glacier.json +84 -0
- package/dist/themes/gruber-darker.json +84 -0
- package/dist/themes/gruvbox-dark-hard.json +84 -0
- package/dist/themes/gruvbox-dark.json +84 -0
- package/dist/themes/gruvbox-material.json +84 -0
- package/dist/themes/guezwhoz.json +84 -0
- package/dist/themes/hacktober.json +84 -0
- package/dist/themes/hardcore.json +84 -0
- package/dist/themes/havn-skumring.json +84 -0
- package/dist/themes/ic-orange-ppl.json +84 -0
- package/dist/themes/iterm2-smoooooth.json +84 -0
- package/dist/themes/iterm2-tango-dark.json +84 -0
- package/dist/themes/japanesque.json +84 -0
- package/dist/themes/jellybeans.json +84 -0
- package/dist/themes/kanagawa-wave.json +84 -0
- package/dist/themes/kurokula.json +84 -0
- package/dist/themes/later-this-evening.json +84 -0
- package/dist/themes/lovelace.json +84 -0
- package/dist/themes/material-darker.json +84 -0
- package/dist/themes/matte-black.json +84 -0
- package/dist/themes/mellow.json +84 -0
- package/dist/themes/miasma.json +84 -0
- package/dist/themes/nord.json +80 -0
- package/dist/themes/nvim-dark.json +84 -0
- package/dist/themes/one-dark.json +80 -0
- package/dist/themes/popping-and-locking.json +84 -0
- package/dist/themes/rose-pine.json +80 -0
- package/dist/themes/sea-shells.json +84 -0
- package/dist/themes/sleepy-hollow.json +84 -0
- package/dist/themes/smyck.json +84 -0
- package/dist/themes/solarized-dark.json +80 -0
- package/dist/themes/tokyo-night.json +80 -0
- package/dist/themes/tomorrow-night-bright.json +84 -0
- package/dist/themes/tomorrow-night-burns.json +84 -0
- package/dist/themes/tomorrow-night.json +84 -0
- package/dist/themes/twilight.json +84 -0
- package/dist/themes/vague.json +84 -0
- package/dist/themes/vesper.json +84 -0
- package/dist/themes/xcode-dark-hc.json +84 -0
- package/docs/custom-provider.md +0 -1
- package/package.json +3 -3
- package/dist/extensions/auto-model/auto-model.test.ts +0 -18
- package/dist/extensions/auto-model/classifier.ts +0 -23
- package/dist/extensions/auto-model/config.ts +0 -51
- package/dist/extensions/auto-model/index.ts +0 -22
- package/dist/extensions/auto-model/lifecycle.test.ts +0 -24
- package/dist/extensions/pi-powerline-footer/.github/workflows/test.yml +0 -27
- package/dist/extensions/pi-powerline-footer/CHANGELOG.md +0 -687
- package/dist/extensions/pi-powerline-footer/CONTRIBUTING.md +0 -16
- package/dist/extensions/pi-powerline-footer/README.md +0 -513
- package/dist/extensions/pi-powerline-footer/banner.png +0 -0
- package/dist/extensions/pi-powerline-footer/bash-mode/completion.ts +0 -586
- package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +0 -583
- package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +0 -204
- package/dist/extensions/pi-powerline-footer/bash-mode/shell-session.ts +0 -286
- package/dist/extensions/pi-powerline-footer/bash-mode/transcript.ts +0 -108
- package/dist/extensions/pi-powerline-footer/bash-mode/types.ts +0 -60
- package/dist/extensions/pi-powerline-footer/cd-command.ts +0 -190
- package/dist/extensions/pi-powerline-footer/colors.ts +0 -70
- package/dist/extensions/pi-powerline-footer/context-usage.ts +0 -156
- package/dist/extensions/pi-powerline-footer/currency-rates.ts +0 -142
- package/dist/extensions/pi-powerline-footer/editor-composition.ts +0 -32
- package/dist/extensions/pi-powerline-footer/editor-performance.ts +0 -99
- package/dist/extensions/pi-powerline-footer/git-status.ts +0 -329
- package/dist/extensions/pi-powerline-footer/guide.ts +0 -220
- package/dist/extensions/pi-powerline-footer/icons.ts +0 -190
- package/dist/extensions/pi-powerline-footer/index.ts +0 -3428
- package/dist/extensions/pi-powerline-footer/lifecycle.ts +0 -10
- package/dist/extensions/pi-powerline-footer/package.json +0 -49
- package/dist/extensions/pi-powerline-footer/paths.ts +0 -22
- package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -446
- package/dist/extensions/pi-powerline-footer/presets.ts +0 -102
- package/dist/extensions/pi-powerline-footer/queue/store.ts +0 -313
- package/dist/extensions/pi-powerline-footer/queue/types.ts +0 -53
- package/dist/extensions/pi-powerline-footer/render-scheduler.ts +0 -29
- package/dist/extensions/pi-powerline-footer/segments.ts +0 -570
- package/dist/extensions/pi-powerline-footer/separators.ts +0 -57
- package/dist/extensions/pi-powerline-footer/session-usage.ts +0 -44
- package/dist/extensions/pi-powerline-footer/shortcuts.ts +0 -61
- package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +0 -1905
- package/dist/extensions/pi-powerline-footer/tests/cd-command.test.ts +0 -144
- package/dist/extensions/pi-powerline-footer/tests/context-usage.test.ts +0 -156
- package/dist/extensions/pi-powerline-footer/tests/custom-items.test.ts +0 -344
- package/dist/extensions/pi-powerline-footer/tests/editor-composition.test.ts +0 -48
- package/dist/extensions/pi-powerline-footer/tests/editor-performance.test.ts +0 -43
- package/dist/extensions/pi-powerline-footer/tests/editor-responsiveness.test.ts +0 -136
- package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +0 -39
- package/dist/extensions/pi-powerline-footer/tests/git-optional-locks.test.ts +0 -64
- package/dist/extensions/pi-powerline-footer/tests/git-status.test.ts +0 -92
- package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +0 -129
- package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -116
- package/dist/extensions/pi-powerline-footer/tests/mode-aware-trigger.test.ts +0 -79
- package/dist/extensions/pi-powerline-footer/tests/paths.test.ts +0 -63
- package/dist/extensions/pi-powerline-footer/tests/queue-store.test.ts +0 -65
- package/dist/extensions/pi-powerline-footer/tests/quit-cursor.test.ts +0 -14
- package/dist/extensions/pi-powerline-footer/tests/remaining-regressions.test.ts +0 -173
- package/dist/extensions/pi-powerline-footer/tests/session-usage.test.ts +0 -47
- package/dist/extensions/pi-powerline-footer/tests/stash-shortcut.test.ts +0 -37
- package/dist/extensions/pi-powerline-footer/tests/thinking-segment.test.ts +0 -76
- package/dist/extensions/pi-powerline-footer/tests/token-stats.test.ts +0 -249
- package/dist/extensions/pi-powerline-footer/tests/tps.test.ts +0 -39
- package/dist/extensions/pi-powerline-footer/tests/usage-display.test.ts +0 -237
- package/dist/extensions/pi-powerline-footer/tests/welcome.test.ts +0 -199
- package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +0 -317
- package/dist/extensions/pi-powerline-footer/theme.example.json +0 -24
- package/dist/extensions/pi-powerline-footer/theme.json +0 -12
- package/dist/extensions/pi-powerline-footer/theme.ts +0 -241
- package/dist/extensions/pi-powerline-footer/token-stats.ts +0 -304
- package/dist/extensions/pi-powerline-footer/tsconfig.json +0 -13
- package/dist/extensions/pi-powerline-footer/types.ts +0 -235
- package/dist/extensions/pi-powerline-footer/welcome-dismiss.ts +0 -34
- package/dist/extensions/pi-powerline-footer/welcome.ts +0 -709
- package/dist/extensions/pi-powerline-footer/working-vibes.ts +0 -721
- package/dist/extensions/preview-tools-disabled.test.ts +0 -37
- package/dist/extensions/preview-tools-disabled.ts +0 -11
- package/dist/themes/powerline-footer/theme.json +0 -33
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
mkdtempSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
realpathSync,
|
|
6
|
+
rmSync,
|
|
7
|
+
symlinkSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { dirname, join } from "node:path";
|
|
13
|
+
import { Container, VStack } from "@earendil-works/pi-tui";
|
|
14
|
+
import { describe, expect, it, vi } from "vitest";
|
|
15
|
+
import {
|
|
16
|
+
type AccentRailLayoutPatchInstallation,
|
|
17
|
+
adjustAccentRailLayoutNode,
|
|
18
|
+
discoverAccentRailLayoutPatchTargetFromEntrypoint,
|
|
19
|
+
installAccentRailLayoutPatchOnTarget,
|
|
20
|
+
markAccentRailLayoutEditor,
|
|
21
|
+
retainAccentRailLayoutPatchInstallation,
|
|
22
|
+
ZENTUI_ACCENT_RAIL_LAYOUT_EDITOR,
|
|
23
|
+
} from "../extensions/zentui/accent-rail-layout-patch";
|
|
24
|
+
import { SessionLifecycle } from "../extensions/zentui/session-lifecycle";
|
|
25
|
+
|
|
26
|
+
const LAYOUT_NODE = Symbol.for("@earendil-works/pi-tui/layout-node");
|
|
27
|
+
const localPiTuiEntry = createRequire(import.meta.url).resolve("@earendil-works/pi-tui");
|
|
28
|
+
const localPiTuiVersion = (
|
|
29
|
+
JSON.parse(readFileSync(join(dirname(localPiTuiEntry), "../package.json"), "utf8")) as {
|
|
30
|
+
version: string;
|
|
31
|
+
}
|
|
32
|
+
).version;
|
|
33
|
+
const localSupportsAccentRailPatch = /^0\.84\.\d+$/.test(localPiTuiVersion);
|
|
34
|
+
const optedInGlobalPi = process.env.ZENTUI_TEST_GLOBAL_PI;
|
|
35
|
+
|
|
36
|
+
type Entry = {
|
|
37
|
+
component: unknown;
|
|
38
|
+
shrink?: number;
|
|
39
|
+
minSize?: number;
|
|
40
|
+
};
|
|
41
|
+
type LayoutMethod = (this: unknown, ...args: unknown[]) => unknown;
|
|
42
|
+
|
|
43
|
+
function markedContainer(
|
|
44
|
+
owner: symbol,
|
|
45
|
+
active: () => boolean = () => true,
|
|
46
|
+
rendered: string[] = ["rail"],
|
|
47
|
+
) {
|
|
48
|
+
const editor = { render: () => rendered };
|
|
49
|
+
const invalidate = vi.fn();
|
|
50
|
+
const container = {
|
|
51
|
+
children: [editor],
|
|
52
|
+
render: vi.fn(() => rendered),
|
|
53
|
+
invalidate,
|
|
54
|
+
};
|
|
55
|
+
expect(markAccentRailLayoutEditor(editor, owner, active)).toBe(true);
|
|
56
|
+
return { editor, container, invalidate };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function makeFakeVStack(method = LAYOUT_NODE) {
|
|
60
|
+
class FakeVStack {
|
|
61
|
+
entries: Entry[] = [];
|
|
62
|
+
layoutType = "vstack";
|
|
63
|
+
gap = 0;
|
|
64
|
+
align = "stretch";
|
|
65
|
+
render = vi.fn(() => ["first", "", "third"]);
|
|
66
|
+
}
|
|
67
|
+
Object.defineProperty(FakeVStack.prototype, method, {
|
|
68
|
+
value(this: FakeVStack) {
|
|
69
|
+
return {
|
|
70
|
+
type: this.layoutType,
|
|
71
|
+
entries: this.entries,
|
|
72
|
+
gap: this.gap,
|
|
73
|
+
align: this.align,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
configurable: true,
|
|
77
|
+
writable: true,
|
|
78
|
+
});
|
|
79
|
+
return FakeVStack;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function installFake(version = "0.84.0", method = LAYOUT_NODE) {
|
|
83
|
+
const FakeVStack = makeFakeVStack(method);
|
|
84
|
+
const owner = Symbol("owner");
|
|
85
|
+
const before = Object.getOwnPropertyDescriptor(FakeVStack.prototype, method);
|
|
86
|
+
const installation = installAccentRailLayoutPatchOnTarget(
|
|
87
|
+
{ prototype: FakeVStack.prototype, version },
|
|
88
|
+
owner,
|
|
89
|
+
);
|
|
90
|
+
return { FakeVStack, owner, before, installation, method };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("Accent Rail fullscreen layout patch", () => {
|
|
94
|
+
it("clones one owned active entry with a stable forwarding component", () => {
|
|
95
|
+
const owner = Symbol("owner");
|
|
96
|
+
const { container, invalidate } = markedContainer(owner);
|
|
97
|
+
const untouched = { component: { children: [] }, shrink: 1, minSize: 1 };
|
|
98
|
+
const entries = [untouched, { component: container, shrink: 1, minSize: 3, basis: "auto" }];
|
|
99
|
+
const node = { type: "vstack", entries, gap: 1, align: "stretch" };
|
|
100
|
+
|
|
101
|
+
const adjusted = adjustAccentRailLayoutNode(node, new Set([owner])) as typeof node;
|
|
102
|
+
const adjustedAgain = adjustAccentRailLayoutNode(node, new Set([owner])) as typeof node;
|
|
103
|
+
const wrapped = adjusted.entries[1]?.component as { render(): string[]; invalidate(): void };
|
|
104
|
+
|
|
105
|
+
expect(adjusted).not.toBe(node);
|
|
106
|
+
expect(adjusted.entries).not.toBe(entries);
|
|
107
|
+
expect(adjusted.entries[0]).toBe(untouched);
|
|
108
|
+
expect(adjusted.entries[1]).toMatchObject({ shrink: 1, minSize: 3, basis: "auto" });
|
|
109
|
+
expect(wrapped).not.toBe(container);
|
|
110
|
+
expect(adjustedAgain.entries[1]?.component).toBe(wrapped);
|
|
111
|
+
expect(wrapped.render()).toEqual(["", "rail"]);
|
|
112
|
+
wrapped.invalidate();
|
|
113
|
+
expect(invalidate).toHaveBeenCalledTimes(1);
|
|
114
|
+
expect(entries[1]).toEqual({
|
|
115
|
+
component: container,
|
|
116
|
+
shrink: 1,
|
|
117
|
+
minSize: 3,
|
|
118
|
+
basis: "auto",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it.each([
|
|
123
|
+
["Opencode or Minimalist live predicate", (): boolean => false, 1, 3],
|
|
124
|
+
["disabled editor live predicate", (): boolean => false, 1, 3],
|
|
125
|
+
["wrong shrink", (): boolean => true, 0, 3],
|
|
126
|
+
["wrong minimum", (): boolean => true, 1, 2],
|
|
127
|
+
] as const)("leaves %s unchanged", (_label, active, shrink, minSize) => {
|
|
128
|
+
const owner = Symbol("owner");
|
|
129
|
+
const { container } = markedContainer(owner, active);
|
|
130
|
+
const node = {
|
|
131
|
+
type: "vstack",
|
|
132
|
+
entries: [{ component: container, shrink, minSize }],
|
|
133
|
+
};
|
|
134
|
+
expect(adjustAccentRailLayoutNode(node, new Set([owner]))).toBe(node);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("leaves native, third-party, and wrapped base editors unmarked", () => {
|
|
138
|
+
const owner = Symbol("owner");
|
|
139
|
+
for (const child of [{}, { render: () => ["native"] }, { render: () => ["third-party"] }]) {
|
|
140
|
+
const node = {
|
|
141
|
+
type: "vstack",
|
|
142
|
+
entries: [{ component: { children: [child] }, shrink: 1, minSize: 3 }],
|
|
143
|
+
};
|
|
144
|
+
expect(adjustAccentRailLayoutNode(node, new Set([owner]))).toBe(node);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("fails open for multiple children, multiple matches, malformed nodes, and throwing markers", () => {
|
|
149
|
+
const owner = Symbol("owner");
|
|
150
|
+
const first = markedContainer(owner).container;
|
|
151
|
+
const second = markedContainer(owner).container;
|
|
152
|
+
const multipleChildren = {
|
|
153
|
+
type: "vstack",
|
|
154
|
+
entries: [{ component: { children: [first.children[0], {}] }, shrink: 1, minSize: 3 }],
|
|
155
|
+
};
|
|
156
|
+
const multipleMatches = {
|
|
157
|
+
type: "vstack",
|
|
158
|
+
entries: [
|
|
159
|
+
{ component: first, shrink: 1, minSize: 3 },
|
|
160
|
+
{ component: second, shrink: 1, minSize: 3 },
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
const throwing = markedContainer(owner, () => {
|
|
164
|
+
throw new Error("stale lifecycle");
|
|
165
|
+
}).container;
|
|
166
|
+
const throwingNode = {
|
|
167
|
+
type: "vstack",
|
|
168
|
+
entries: [{ component: throwing, shrink: 1, minSize: 3 }],
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
expect(adjustAccentRailLayoutNode(multipleChildren, new Set([owner]))).toBe(multipleChildren);
|
|
172
|
+
expect(adjustAccentRailLayoutNode(multipleMatches, new Set([owner]))).toBe(multipleMatches);
|
|
173
|
+
expect(adjustAccentRailLayoutNode(throwingNode, new Set([owner]))).toBe(throwingNode);
|
|
174
|
+
expect(adjustAccentRailLayoutNode(null, new Set([owner]))).toBeNull();
|
|
175
|
+
expect(adjustAccentRailLayoutNode({ type: "hstack", entries: [] }, new Set([owner]))).toEqual({
|
|
176
|
+
type: "hstack",
|
|
177
|
+
entries: [],
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("marks only the requested outer editor with a non-enumerable own marker", () => {
|
|
182
|
+
const owner = Symbol("owner");
|
|
183
|
+
const outer = {};
|
|
184
|
+
const base = {};
|
|
185
|
+
expect(markAccentRailLayoutEditor(outer, owner, () => true)).toBe(true);
|
|
186
|
+
expect(Object.keys(outer)).not.toContain(String(ZENTUI_ACCENT_RAIL_LAYOUT_EDITOR));
|
|
187
|
+
expect(Object.hasOwn(outer, ZENTUI_ACCENT_RAIL_LAYOUT_EDITOR)).toBe(true);
|
|
188
|
+
expect(Object.hasOwn(base, ZENTUI_ACCENT_RAIL_LAYOUT_EDITOR)).toBe(false);
|
|
189
|
+
expect(markAccentRailLayoutEditor(Object.freeze({}), owner, () => true)).toBe(false);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("disposes a delayed installation when its lifecycle generation becomes stale", async () => {
|
|
193
|
+
let resolveInstallation: ((value: AccentRailLayoutPatchInstallation) => void) | undefined;
|
|
194
|
+
const cleanup = vi.fn();
|
|
195
|
+
const retained = vi.fn();
|
|
196
|
+
const lifecycle = new SessionLifecycle();
|
|
197
|
+
const lifecycleGeneration = lifecycle.start();
|
|
198
|
+
let installSerial = 1;
|
|
199
|
+
const capturedSerial = installSerial;
|
|
200
|
+
const pending = retainAccentRailLayoutPatchInstallation(
|
|
201
|
+
() =>
|
|
202
|
+
new Promise((resolve) => {
|
|
203
|
+
resolveInstallation = resolve;
|
|
204
|
+
}),
|
|
205
|
+
() => lifecycle.isCurrent(lifecycleGeneration) && capturedSerial === installSerial,
|
|
206
|
+
retained,
|
|
207
|
+
);
|
|
208
|
+
lifecycle.shutdown();
|
|
209
|
+
installSerial += 1;
|
|
210
|
+
resolveInstallation?.({ cleanup, diagnostic: "installed", version: "0.84.2" });
|
|
211
|
+
expect(await pending).toBe("stale");
|
|
212
|
+
expect(cleanup).toHaveBeenCalledOnce();
|
|
213
|
+
expect(retained).not.toHaveBeenCalled();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("retains only the newest delayed installation across a restart race", async () => {
|
|
217
|
+
type Resolver = (value: AccentRailLayoutPatchInstallation) => void;
|
|
218
|
+
let resolveFirst: Resolver | undefined;
|
|
219
|
+
let resolveSecond: Resolver | undefined;
|
|
220
|
+
const lifecycle = new SessionLifecycle();
|
|
221
|
+
const firstGeneration = lifecycle.start();
|
|
222
|
+
let installSerial = 1;
|
|
223
|
+
const firstSerial = installSerial;
|
|
224
|
+
const firstCleanup = vi.fn();
|
|
225
|
+
const secondCleanup = vi.fn();
|
|
226
|
+
const retained: AccentRailLayoutPatchInstallation[] = [];
|
|
227
|
+
const first = retainAccentRailLayoutPatchInstallation(
|
|
228
|
+
() => new Promise((resolve) => (resolveFirst = resolve)),
|
|
229
|
+
() => lifecycle.isCurrent(firstGeneration) && installSerial === firstSerial,
|
|
230
|
+
(installation) => retained.push(installation),
|
|
231
|
+
);
|
|
232
|
+
const secondGeneration = lifecycle.start();
|
|
233
|
+
installSerial += 1;
|
|
234
|
+
const secondSerial = installSerial;
|
|
235
|
+
const second = retainAccentRailLayoutPatchInstallation(
|
|
236
|
+
() => new Promise((resolve) => (resolveSecond = resolve)),
|
|
237
|
+
() => lifecycle.isCurrent(secondGeneration) && installSerial === secondSerial,
|
|
238
|
+
(installation) => retained.push(installation),
|
|
239
|
+
);
|
|
240
|
+
resolveSecond?.({ cleanup: secondCleanup, diagnostic: "reused", version: "0.84.2" });
|
|
241
|
+
expect(await second).toBe("retained");
|
|
242
|
+
resolveFirst?.({ cleanup: firstCleanup, diagnostic: "installed", version: "0.84.2" });
|
|
243
|
+
expect(await first).toBe("stale");
|
|
244
|
+
expect(firstCleanup).toHaveBeenCalledOnce();
|
|
245
|
+
expect(secondCleanup).not.toHaveBeenCalled();
|
|
246
|
+
expect(retained).toHaveLength(1);
|
|
247
|
+
expect(retained[0]?.cleanup).toBe(secondCleanup);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("fails open when asynchronous host discovery rejects", async () => {
|
|
251
|
+
const retained = vi.fn();
|
|
252
|
+
expect(
|
|
253
|
+
await retainAccentRailLayoutPatchInstallation(
|
|
254
|
+
async () => {
|
|
255
|
+
throw new Error("host discovery failed");
|
|
256
|
+
},
|
|
257
|
+
() => true,
|
|
258
|
+
retained,
|
|
259
|
+
),
|
|
260
|
+
).toBe("failed");
|
|
261
|
+
expect(retained).not.toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("disposes the installation when retaining its cleanup throws", async () => {
|
|
265
|
+
const cleanup = vi.fn();
|
|
266
|
+
expect(
|
|
267
|
+
await retainAccentRailLayoutPatchInstallation(
|
|
268
|
+
async () => ({ cleanup, diagnostic: "installed", version: "0.84.2" }),
|
|
269
|
+
() => true,
|
|
270
|
+
() => {
|
|
271
|
+
throw new Error("stale cleanup slot");
|
|
272
|
+
},
|
|
273
|
+
),
|
|
274
|
+
).toBe("failed");
|
|
275
|
+
expect(cleanup).toHaveBeenCalledOnce();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("responds to live style and enablement predicates without reinstalling", () => {
|
|
279
|
+
const { FakeVStack, owner, installation } = installFake();
|
|
280
|
+
let active = true;
|
|
281
|
+
const stack = new FakeVStack();
|
|
282
|
+
const { container } = markedContainer(owner, () => active);
|
|
283
|
+
stack.entries = [{ component: container, shrink: 1, minSize: 3 }];
|
|
284
|
+
const layout = () =>
|
|
285
|
+
(stack as unknown as Record<symbol, () => { entries: Entry[] }>)[LAYOUT_NODE]?.()?.entries[0];
|
|
286
|
+
expect(layout()?.minSize).toBe(3);
|
|
287
|
+
expect(layout()?.component).not.toBe(container);
|
|
288
|
+
active = false;
|
|
289
|
+
expect(layout()?.component).toBe(container);
|
|
290
|
+
active = true;
|
|
291
|
+
expect(layout()?.component).not.toBe(container);
|
|
292
|
+
installation.cleanup();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("patches only the layout-node method and preserves regular rendering and intrinsic rows", () => {
|
|
296
|
+
const { FakeVStack, owner, installation } = installFake();
|
|
297
|
+
const stack = new FakeVStack();
|
|
298
|
+
const { container } = markedContainer(owner);
|
|
299
|
+
stack.entries = [{ component: container, shrink: 1, minSize: 3 }];
|
|
300
|
+
const intrinsicRows = ["rail", "", "multiline", "viewport", "autocomplete"];
|
|
301
|
+
stack.render.mockReturnValue(intrinsicRows);
|
|
302
|
+
|
|
303
|
+
const node = (stack as unknown as Record<symbol, () => unknown>)[LAYOUT_NODE]?.() as {
|
|
304
|
+
entries: Entry[];
|
|
305
|
+
};
|
|
306
|
+
const entry = node.entries[0];
|
|
307
|
+
expect(entry).toBeDefined();
|
|
308
|
+
if (!entry) throw new Error("expected patched layout entry");
|
|
309
|
+
expect(entry.minSize).toBe(3);
|
|
310
|
+
expect((entry.component as { render(): string[] }).render()).toEqual(["", "rail"]);
|
|
311
|
+
expect(stack.render()).toBe(intrinsicRows);
|
|
312
|
+
expect(stack.render).toHaveBeenCalledTimes(1);
|
|
313
|
+
installation.cleanup();
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("moves one-row Accent Rail down while keeping Footer in the final dock row", () => {
|
|
317
|
+
const owner = Symbol("owner");
|
|
318
|
+
const { container } = markedContainer(owner);
|
|
319
|
+
const footer = { render: () => ["footer"] };
|
|
320
|
+
const node = adjustAccentRailLayoutNode(
|
|
321
|
+
{
|
|
322
|
+
type: "vstack",
|
|
323
|
+
entries: [
|
|
324
|
+
{ component: container, shrink: 1, minSize: 3 },
|
|
325
|
+
{ component: footer, shrink: 1, minSize: 1 },
|
|
326
|
+
],
|
|
327
|
+
gap: 0,
|
|
328
|
+
},
|
|
329
|
+
new Set([owner]),
|
|
330
|
+
) as { entries: Entry[] };
|
|
331
|
+
|
|
332
|
+
expect(node.entries[0]?.minSize).toBe(3);
|
|
333
|
+
expect(node.entries[0]?.component).not.toBe(container);
|
|
334
|
+
const wrapped = node.entries[0]?.component as { render(): string[] };
|
|
335
|
+
const oneRow = wrapped.render();
|
|
336
|
+
expect(oneRow).toEqual(["", "rail"]);
|
|
337
|
+
const paddedOneRow = [...oneRow];
|
|
338
|
+
while (paddedOneRow.length < 3) paddedOneRow.push("");
|
|
339
|
+
expect([...paddedOneRow, "footer"]).toEqual(["", "rail", "", "footer"]);
|
|
340
|
+
|
|
341
|
+
for (const intrinsicRows of [
|
|
342
|
+
[],
|
|
343
|
+
["rail", "autocomplete"],
|
|
344
|
+
["rail", ""],
|
|
345
|
+
["above", "rail"],
|
|
346
|
+
["rail", "below"],
|
|
347
|
+
["rail", "", "multiline"],
|
|
348
|
+
]) {
|
|
349
|
+
container.render.mockReturnValueOnce(intrinsicRows);
|
|
350
|
+
const rendered = wrapped.render();
|
|
351
|
+
expect(rendered).toBe(intrinsicRows);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it("supports multiple owners and restores the exact predecessor descriptor after the last cleanup", () => {
|
|
356
|
+
const { FakeVStack, before, installation, method } = installFake();
|
|
357
|
+
const secondOwner = Symbol("second-owner");
|
|
358
|
+
const second = installAccentRailLayoutPatchOnTarget(
|
|
359
|
+
{ prototype: FakeVStack.prototype, version: "0.84.2" },
|
|
360
|
+
secondOwner,
|
|
361
|
+
);
|
|
362
|
+
expect(second.diagnostic).toBe("reused");
|
|
363
|
+
|
|
364
|
+
installation.cleanup();
|
|
365
|
+
expect(Object.getOwnPropertyDescriptor(FakeVStack.prototype, method)?.value).not.toBe(
|
|
366
|
+
before?.value,
|
|
367
|
+
);
|
|
368
|
+
second.cleanup();
|
|
369
|
+
expect(Object.getOwnPropertyDescriptor(FakeVStack.prototype, method)).toEqual(before);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("reference-counts repeated registrations for one owner", () => {
|
|
373
|
+
const { FakeVStack, owner, before, installation, method } = installFake();
|
|
374
|
+
const second = installAccentRailLayoutPatchOnTarget(
|
|
375
|
+
{ prototype: FakeVStack.prototype, version: "0.84.0" },
|
|
376
|
+
owner,
|
|
377
|
+
);
|
|
378
|
+
installation.cleanup();
|
|
379
|
+
expect(Object.getOwnPropertyDescriptor(FakeVStack.prototype, method)?.value).not.toBe(
|
|
380
|
+
before?.value,
|
|
381
|
+
);
|
|
382
|
+
second.cleanup();
|
|
383
|
+
expect(Object.getOwnPropertyDescriptor(FakeVStack.prototype, method)).toEqual(before);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it("preserves a successor and keeps a later-restored ownerless wrapper inert", () => {
|
|
387
|
+
const { FakeVStack, owner, installation, method } = installFake();
|
|
388
|
+
const secondOwner = Symbol("second-owner");
|
|
389
|
+
const second = installAccentRailLayoutPatchOnTarget(
|
|
390
|
+
{ prototype: FakeVStack.prototype, version: "0.84.2" },
|
|
391
|
+
secondOwner,
|
|
392
|
+
);
|
|
393
|
+
const zentuiWrapper = (FakeVStack.prototype as unknown as Record<symbol, LayoutMethod>)[method];
|
|
394
|
+
const successor = function successorPatch(this: unknown, ...args: unknown[]) {
|
|
395
|
+
return Reflect.apply(zentuiWrapper, this, args);
|
|
396
|
+
};
|
|
397
|
+
Object.defineProperty(FakeVStack.prototype, method, {
|
|
398
|
+
...Object.getOwnPropertyDescriptor(FakeVStack.prototype, method),
|
|
399
|
+
value: successor,
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
const displaced = installAccentRailLayoutPatchOnTarget(
|
|
403
|
+
{ prototype: FakeVStack.prototype, version: "0.84.2" },
|
|
404
|
+
Symbol("late-owner"),
|
|
405
|
+
);
|
|
406
|
+
expect(displaced.diagnostic).toBe("displaced");
|
|
407
|
+
installation.cleanup();
|
|
408
|
+
second.cleanup();
|
|
409
|
+
expect((FakeVStack.prototype as unknown as Record<symbol, unknown>)[method]).toBe(successor);
|
|
410
|
+
|
|
411
|
+
// Simulate the successor restoring the predecessor it captured during install.
|
|
412
|
+
Object.defineProperty(FakeVStack.prototype, method, {
|
|
413
|
+
...Object.getOwnPropertyDescriptor(FakeVStack.prototype, method),
|
|
414
|
+
value: zentuiWrapper,
|
|
415
|
+
});
|
|
416
|
+
const stack = new FakeVStack();
|
|
417
|
+
const { container } = markedContainer(owner);
|
|
418
|
+
stack.entries = [{ component: container, shrink: 1, minSize: 3 }];
|
|
419
|
+
const restored = (stack as unknown as Record<symbol, () => { entries: Entry[] }>)[method]();
|
|
420
|
+
expect(restored.entries[0]?.component).toBe(container);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("does not install for unsupported Pi TUI versions", () => {
|
|
424
|
+
for (const version of ["0.80.5", "0.82.1", "0.85.0", "unknown"]) {
|
|
425
|
+
const { FakeVStack, before, installation, method } = installFake(version);
|
|
426
|
+
expect(installation.diagnostic).toBe("unsupported-version");
|
|
427
|
+
expect(Object.getOwnPropertyDescriptor(FakeVStack.prototype, method)).toEqual(before);
|
|
428
|
+
}
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it("discovers a structurally compatible non-exported layout symbol", () => {
|
|
432
|
+
const alternate = Symbol("alternate-layout-node");
|
|
433
|
+
const { FakeVStack, owner, installation } = installFake("0.84.2", alternate);
|
|
434
|
+
const stack = new FakeVStack();
|
|
435
|
+
stack.entries = [{ component: markedContainer(owner).container, shrink: 1, minSize: 3 }];
|
|
436
|
+
const node = (stack as unknown as Record<symbol, () => unknown>)[alternate]?.() as {
|
|
437
|
+
entries: Entry[];
|
|
438
|
+
};
|
|
439
|
+
expect(installation.diagnostic).toBe("installed");
|
|
440
|
+
const entry = node.entries[0];
|
|
441
|
+
expect(entry).toBeDefined();
|
|
442
|
+
if (!entry) throw new Error("expected alternate layout entry");
|
|
443
|
+
expect(entry.minSize).toBe(3);
|
|
444
|
+
expect((entry.component as { render(): string[] }).render()).toEqual(["", "rail"]);
|
|
445
|
+
installation.cleanup();
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it.skipIf(!localSupportsAccentRailPatch)(
|
|
449
|
+
"patches the checked-in 0.84.x VStack without mutating its entries",
|
|
450
|
+
() => {
|
|
451
|
+
const owner = Symbol("owner");
|
|
452
|
+
const editor = { render: () => ["rail"], invalidate() {} };
|
|
453
|
+
markAccentRailLayoutEditor(editor, owner, () => true);
|
|
454
|
+
const container = new Container();
|
|
455
|
+
container.addChild(editor);
|
|
456
|
+
const stack = new VStack([{ component: container, shrink: 1, minSize: 3 }]);
|
|
457
|
+
const installation = installAccentRailLayoutPatchOnTarget(
|
|
458
|
+
{ prototype: VStack.prototype, version: localPiTuiVersion },
|
|
459
|
+
owner,
|
|
460
|
+
);
|
|
461
|
+
const originalEntry = (stack as unknown as Record<symbol, () => { entries: Entry[] }>)[
|
|
462
|
+
LAYOUT_NODE
|
|
463
|
+
]?.()?.entries[0];
|
|
464
|
+
expect(originalEntry).toBeDefined();
|
|
465
|
+
if (!originalEntry) throw new Error("expected checked-in layout entry");
|
|
466
|
+
expect(originalEntry.minSize).toBe(3);
|
|
467
|
+
expect(originalEntry.component).not.toBe(container);
|
|
468
|
+
expect((originalEntry.component as { render(): string[] }).render()).toEqual(["", "rail"]);
|
|
469
|
+
expect((stack as unknown as { entries: Entry[] }).entries[0]?.minSize).toBe(3);
|
|
470
|
+
installation.cleanup();
|
|
471
|
+
},
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
it.skipIf(!localSupportsAccentRailPatch)(
|
|
475
|
+
"discovers the checked-in coding-agent nested host package instance",
|
|
476
|
+
async () => {
|
|
477
|
+
const require = createRequire(import.meta.url);
|
|
478
|
+
const tuiEntry = require.resolve("@earendil-works/pi-tui");
|
|
479
|
+
const entrypoint = join(dirname(tuiEntry), "../../pi-coding-agent/dist/cli.js");
|
|
480
|
+
const target = await discoverAccentRailLayoutPatchTargetFromEntrypoint(entrypoint);
|
|
481
|
+
expect(target?.version).toBe(localPiTuiVersion);
|
|
482
|
+
expect(target?.resolvedModulePath).not.toBe(target?.localModulePath);
|
|
483
|
+
expect(target?.resolvedModulePath).toContain(
|
|
484
|
+
"pi-coding-agent/node_modules/@earendil-works/pi-tui",
|
|
485
|
+
);
|
|
486
|
+
expect(target?.prototype).not.toBe(VStack.prototype);
|
|
487
|
+
},
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
it.skipIf(!localSupportsAccentRailPatch)(
|
|
491
|
+
"deduplicates host and local resolution when both canonicalize to one module",
|
|
492
|
+
async () => {
|
|
493
|
+
const fixture = makeHostPackageFixture(true);
|
|
494
|
+
try {
|
|
495
|
+
const target = await discoverAccentRailLayoutPatchTargetFromEntrypoint(fixture.bin);
|
|
496
|
+
expect(target?.version).toBe(localPiTuiVersion);
|
|
497
|
+
expect(target?.resolvedModulePath).toBe(target?.localModulePath);
|
|
498
|
+
expect(target?.resolvedModulePath).toBe(realpathSync(fixture.tuiEntry));
|
|
499
|
+
} finally {
|
|
500
|
+
fixture.cleanup();
|
|
501
|
+
}
|
|
502
|
+
},
|
|
503
|
+
);
|
|
504
|
+
|
|
505
|
+
it("canonicalizes an npm-bin symlink and patches only the distinct host VStack", async () => {
|
|
506
|
+
const fixture = makeHostPackageFixture();
|
|
507
|
+
try {
|
|
508
|
+
const target = await discoverAccentRailLayoutPatchTargetFromEntrypoint(fixture.bin);
|
|
509
|
+
expect(target?.canonicalEntrypoint).toBe(realpathSync(fixture.cli));
|
|
510
|
+
expect(target?.resolvedModulePath).toBe(realpathSync(fixture.tuiEntry));
|
|
511
|
+
expect(target?.resolvedModulePath).not.toBe(target?.localModulePath);
|
|
512
|
+
if (typeof VStack === "function") expect(target?.prototype).not.toBe(VStack.prototype);
|
|
513
|
+
|
|
514
|
+
const owner = Symbol("host-owner");
|
|
515
|
+
const installation = target ? installAccentRailLayoutPatchOnTarget(target, owner) : undefined;
|
|
516
|
+
expect(installation?.diagnostic).toBe("installed");
|
|
517
|
+
const hostStack = Object.create(target?.prototype ?? null) as Record<PropertyKey, unknown>;
|
|
518
|
+
hostStack.entries = [
|
|
519
|
+
{
|
|
520
|
+
component: markedContainer(owner).container,
|
|
521
|
+
shrink: 1,
|
|
522
|
+
minSize: 3,
|
|
523
|
+
},
|
|
524
|
+
];
|
|
525
|
+
hostStack.layoutType = "vstack";
|
|
526
|
+
hostStack.gap = 0;
|
|
527
|
+
hostStack.align = "stretch";
|
|
528
|
+
const hostNode = (hostStack[LAYOUT_NODE] as () => { entries: Entry[] })();
|
|
529
|
+
const hostEntry = hostNode.entries[0];
|
|
530
|
+
expect(hostEntry).toBeDefined();
|
|
531
|
+
if (!hostEntry) throw new Error("expected host layout entry");
|
|
532
|
+
expect(hostEntry.minSize).toBe(3);
|
|
533
|
+
expect((hostEntry.component as { render(): string[] }).render()).toEqual(["", "rail"]);
|
|
534
|
+
|
|
535
|
+
if (typeof VStack === "function" && typeof Container === "function") {
|
|
536
|
+
const localStack = new VStack([{ component: new Container(), shrink: 1, minSize: 3 }]);
|
|
537
|
+
const localNode = (localStack as unknown as Record<symbol, () => { entries: Entry[] }>)[
|
|
538
|
+
LAYOUT_NODE
|
|
539
|
+
]?.();
|
|
540
|
+
expect(localNode.entries[0]?.minSize).toBe(3);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
installation?.cleanup();
|
|
544
|
+
const restoredNode = (hostStack[LAYOUT_NODE] as () => { entries: Entry[] })();
|
|
545
|
+
expect(restoredNode.entries[0]?.minSize).toBe(3);
|
|
546
|
+
} finally {
|
|
547
|
+
fixture.cleanup();
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it("fails open for wrappers, directories, missing paths, and broken symlinks", async () => {
|
|
552
|
+
const root = mkdtempSync(join(tmpdir(), "zentui-layout-invalid-"));
|
|
553
|
+
try {
|
|
554
|
+
const wrapper = join(root, "pi-wrapper.js");
|
|
555
|
+
writeFileSync(wrapper, "console.log('wrapper')\n");
|
|
556
|
+
const broken = join(root, "pi-broken");
|
|
557
|
+
symlinkSync(join(root, "missing-cli.js"), broken);
|
|
558
|
+
expect(await discoverAccentRailLayoutPatchTargetFromEntrypoint(wrapper)).toBeUndefined();
|
|
559
|
+
expect(await discoverAccentRailLayoutPatchTargetFromEntrypoint(root)).toBeUndefined();
|
|
560
|
+
expect(
|
|
561
|
+
await discoverAccentRailLayoutPatchTargetFromEntrypoint(join(root, "missing")),
|
|
562
|
+
).toBeUndefined();
|
|
563
|
+
expect(await discoverAccentRailLayoutPatchTargetFromEntrypoint(broken)).toBeUndefined();
|
|
564
|
+
} finally {
|
|
565
|
+
rmSync(root, { force: true, recursive: true });
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it.skipIf(!optedInGlobalPi)(
|
|
570
|
+
"patches an explicitly selected compatible global Pi 0.84.x host",
|
|
571
|
+
async () => {
|
|
572
|
+
const target = await discoverAccentRailLayoutPatchTargetFromEntrypoint(
|
|
573
|
+
optedInGlobalPi as string,
|
|
574
|
+
);
|
|
575
|
+
expect(target?.version).toMatch(/^0\.84\.\d+$/);
|
|
576
|
+
expect(target?.canonicalEntrypoint).toContain("pi-coding-agent/dist/cli.js");
|
|
577
|
+
if (typeof VStack === "function") expect(target?.prototype).not.toBe(VStack.prototype);
|
|
578
|
+
const owner = Symbol("global-owner");
|
|
579
|
+
const installation = target ? installAccentRailLayoutPatchOnTarget(target, owner) : undefined;
|
|
580
|
+
expect(installation?.diagnostic).toBe("installed");
|
|
581
|
+
const { container } = markedContainer(owner);
|
|
582
|
+
const receiver = Object.create(target?.prototype ?? null) as Record<PropertyKey, unknown>;
|
|
583
|
+
receiver.entries = [{ component: container, shrink: 1, minSize: 3 }];
|
|
584
|
+
receiver.layoutType = "vstack";
|
|
585
|
+
receiver.gap = 0;
|
|
586
|
+
receiver.align = "stretch";
|
|
587
|
+
const node = (receiver[LAYOUT_NODE] as () => { entries: Entry[] })();
|
|
588
|
+
const adjustedComponent = node.entries[0]?.component;
|
|
589
|
+
if (!adjustedComponent) throw new Error("missing adjusted layout component");
|
|
590
|
+
expect(node.entries[0]?.minSize).toBe(3);
|
|
591
|
+
expect(adjustedComponent).not.toBe(container);
|
|
592
|
+
expect((adjustedComponent as { render(): string[] }).render()).toEqual(["", "rail"]);
|
|
593
|
+
if (typeof VStack === "function" && typeof Container === "function") {
|
|
594
|
+
expect(
|
|
595
|
+
(
|
|
596
|
+
new VStack([
|
|
597
|
+
{ component: new Container(), shrink: 1, minSize: 3 },
|
|
598
|
+
]) as unknown as Record<symbol, () => { entries: Entry[] }>
|
|
599
|
+
)[LAYOUT_NODE]?.().entries[0]?.minSize,
|
|
600
|
+
).toBe(3);
|
|
601
|
+
}
|
|
602
|
+
installation?.cleanup();
|
|
603
|
+
const restored = (receiver[LAYOUT_NODE] as () => { entries: Entry[] })();
|
|
604
|
+
const restoredComponent = restored.entries[0]?.component;
|
|
605
|
+
if (!restoredComponent) throw new Error("missing restored layout component");
|
|
606
|
+
expect(restored.entries[0]?.minSize).toBe(3);
|
|
607
|
+
expect(restoredComponent).toBe(container);
|
|
608
|
+
expect((restoredComponent as { render(): string[] }).render()).toEqual(["rail"]);
|
|
609
|
+
},
|
|
610
|
+
);
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
function makeHostPackageFixture(useLocalTui = false): {
|
|
614
|
+
root: string;
|
|
615
|
+
bin: string;
|
|
616
|
+
cli: string;
|
|
617
|
+
tuiEntry: string;
|
|
618
|
+
cleanup: () => void;
|
|
619
|
+
} {
|
|
620
|
+
const root = mkdtempSync(join(tmpdir(), "zentui-layout-host-"));
|
|
621
|
+
const codingRoot = join(root, "node_modules/@earendil-works/pi-coding-agent");
|
|
622
|
+
const tuiRoot = join(root, "node_modules/@earendil-works/pi-tui");
|
|
623
|
+
const cli = join(codingRoot, "dist/cli.js");
|
|
624
|
+
const tuiEntry = join(tuiRoot, "index.js");
|
|
625
|
+
let expectedTuiEntry = tuiEntry;
|
|
626
|
+
const bin = join(root, "bin/pi");
|
|
627
|
+
mkdirSync(dirname(cli), { recursive: true });
|
|
628
|
+
mkdirSync(dirname(tuiRoot), { recursive: true });
|
|
629
|
+
mkdirSync(dirname(bin), { recursive: true });
|
|
630
|
+
writeFileSync(
|
|
631
|
+
join(codingRoot, "package.json"),
|
|
632
|
+
JSON.stringify({ name: "@earendil-works/pi-coding-agent", version: "0.84.2", type: "module" }),
|
|
633
|
+
);
|
|
634
|
+
writeFileSync(cli, "export {};\n");
|
|
635
|
+
if (useLocalTui) {
|
|
636
|
+
const localEntry = createRequire(import.meta.url).resolve("@earendil-works/pi-tui");
|
|
637
|
+
const localRoot = join(dirname(localEntry), "..");
|
|
638
|
+
symlinkSync(localRoot, tuiRoot, "dir");
|
|
639
|
+
expectedTuiEntry = localEntry;
|
|
640
|
+
} else {
|
|
641
|
+
mkdirSync(tuiRoot, { recursive: true });
|
|
642
|
+
writeFileSync(
|
|
643
|
+
join(tuiRoot, "package.json"),
|
|
644
|
+
JSON.stringify({
|
|
645
|
+
name: "@earendil-works/pi-tui",
|
|
646
|
+
version: "0.84.2",
|
|
647
|
+
type: "module",
|
|
648
|
+
main: "./index.js",
|
|
649
|
+
}),
|
|
650
|
+
);
|
|
651
|
+
writeFileSync(
|
|
652
|
+
tuiEntry,
|
|
653
|
+
`const layout = Symbol.for("@earendil-works/pi-tui/layout-node");
|
|
654
|
+
export class VStack {
|
|
655
|
+
constructor(entries = []) { this.entries = entries; this.layoutType = "vstack"; this.gap = 0; this.align = "stretch"; }
|
|
656
|
+
[layout]() { return { type: this.layoutType, entries: this.entries, gap: this.gap, align: this.align }; }
|
|
657
|
+
}
|
|
658
|
+
`,
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
symlinkSync(cli, bin);
|
|
662
|
+
return {
|
|
663
|
+
root,
|
|
664
|
+
bin,
|
|
665
|
+
cli,
|
|
666
|
+
tuiEntry: expectedTuiEntry,
|
|
667
|
+
cleanup: () => rmSync(root, { force: true, recursive: true }),
|
|
668
|
+
};
|
|
669
|
+
}
|