@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,405 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
import { defaultConfig } from "../extensions/zentui/config";
|
|
6
|
+
import {
|
|
7
|
+
type EditorMetadataValues,
|
|
8
|
+
renderEditorMetadataFormat,
|
|
9
|
+
renderEditorMetadataFormatSplit,
|
|
10
|
+
sanitizeEditorMetadataText,
|
|
11
|
+
} from "../extensions/zentui/editor-metadata-format";
|
|
12
|
+
import { composeEditorMetadataLine, renderPolishedEditorFrame } from "../extensions/zentui/ui";
|
|
13
|
+
|
|
14
|
+
function makeTheme(): Theme {
|
|
15
|
+
return {
|
|
16
|
+
fg(color: string, text: string) {
|
|
17
|
+
return `[${color}]${text}`;
|
|
18
|
+
},
|
|
19
|
+
bold(text: string) {
|
|
20
|
+
return text;
|
|
21
|
+
},
|
|
22
|
+
italic(text: string) {
|
|
23
|
+
return text;
|
|
24
|
+
},
|
|
25
|
+
underline(text: string) {
|
|
26
|
+
return text;
|
|
27
|
+
},
|
|
28
|
+
} as unknown as Theme;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeAnsiTheme(): Theme {
|
|
32
|
+
return {
|
|
33
|
+
fg(_color: string, text: string) {
|
|
34
|
+
return `\x1b[31m${text}\x1b[39m`;
|
|
35
|
+
},
|
|
36
|
+
bold(text: string) {
|
|
37
|
+
return text;
|
|
38
|
+
},
|
|
39
|
+
italic(text: string) {
|
|
40
|
+
return text;
|
|
41
|
+
},
|
|
42
|
+
underline(text: string) {
|
|
43
|
+
return text;
|
|
44
|
+
},
|
|
45
|
+
} as unknown as Theme;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const values: EditorMetadataValues = {
|
|
49
|
+
model: "Model Label",
|
|
50
|
+
modelId: "model-id",
|
|
51
|
+
modelName: "Model Name",
|
|
52
|
+
provider: "Provider",
|
|
53
|
+
thinking: "high",
|
|
54
|
+
sessionName: "Session",
|
|
55
|
+
contextPercent: 26.8,
|
|
56
|
+
contextWindow: 272_000,
|
|
57
|
+
inputTokens: 76_000,
|
|
58
|
+
outputTokens: 1_600,
|
|
59
|
+
cacheHitRate: 73.4,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
function render(format: string, overrides: Partial<EditorMetadataValues> = {}): string {
|
|
63
|
+
return renderEditorMetadataFormat(
|
|
64
|
+
format,
|
|
65
|
+
{ ...values, ...overrides },
|
|
66
|
+
makeTheme(),
|
|
67
|
+
defaultConfig,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
describe("renderEditorMetadataFormat", () => {
|
|
72
|
+
it("renders all supported variables in both syntaxes", () => {
|
|
73
|
+
expect(render(`$model|\${model_id}|$model_name|\${provider}|$thinking|\${session_name}`)).toBe(
|
|
74
|
+
"[accent]Model Label[border]|[accent]model-id[border]|[accent]Model Name[border]|[text]Provider[border]|[muted]high[border]|[border]Session",
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("renders usage variables in both syntaxes without duplicating cache metadata", () => {
|
|
79
|
+
expect(render(`$context | \${tokens} | $cache_hit`)).toBe(
|
|
80
|
+
"[border]26.8%/272k[border] | [border]↑76k ↓1.6k[border] | [border]73.4%",
|
|
81
|
+
);
|
|
82
|
+
expect(render(`\${context} | $tokens | \${cache_hit}`)).toBe(
|
|
83
|
+
"[border]26.8%/272k[border] | [border]↑76k ↓1.6k[border] | [border]73.4%",
|
|
84
|
+
);
|
|
85
|
+
expect(render("$tokens")).not.toContain("73.4%");
|
|
86
|
+
expect(render("$cache_hit")).not.toMatch(/[↑↓]/);
|
|
87
|
+
expect(render("($context)( · $tokens)( · $cache_hit)")).toBe(
|
|
88
|
+
"[border]26.8%/272k[border] · [border]↑76k ↓1.6k[border] · [border]73.4%",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("renders unavailable cache-hit metadata as zero", () => {
|
|
93
|
+
expect(render("before( · $cache_hit)", { cacheHitRate: undefined })).toBe(
|
|
94
|
+
"[border]before[border] · [border]0.0%",
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("uses conditional groups for optional values", () => {
|
|
99
|
+
expect(render("$model( · $thinking)", { thinking: "off" })).toBe("[accent]Model Label");
|
|
100
|
+
expect(render("$model( · $thinking)")).toBe("[accent]Model Label[border] · [muted]high");
|
|
101
|
+
expect(
|
|
102
|
+
render("$model( · $model_name)( · $session_name)", { modelName: "", sessionName: "" }),
|
|
103
|
+
).toBe("[accent]Model Label");
|
|
104
|
+
expect(render("before((literal))after")).toBe("[border]before[border]literal[border]after");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("renders unknown variables and fill as empty", () => {
|
|
108
|
+
expect(render("a$unknown-b$fill-c($unknown)($fill)")).toBe("[border]a[border]-b[border]-c");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("preserves the default layout and hides thinking when off", () => {
|
|
112
|
+
expect(render(defaultConfig.editorMetadataFormat, { thinking: "off" })).toBe(
|
|
113
|
+
"[accent]Model Label[border] [text]Provider",
|
|
114
|
+
);
|
|
115
|
+
expect(render(defaultConfig.editorMetadataFormat)).toBe(
|
|
116
|
+
"[accent]Model Label[border] [text]Provider[border] [muted]high",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("routes semantic and neutral styles", () => {
|
|
121
|
+
const config = {
|
|
122
|
+
...defaultConfig,
|
|
123
|
+
colors: {
|
|
124
|
+
...defaultConfig.colors,
|
|
125
|
+
editorModel: "success",
|
|
126
|
+
editorProvider: "syntaxKeyword",
|
|
127
|
+
editorThinking: "thinkingText",
|
|
128
|
+
editorThinkingHigh: "thinkingHigh",
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
const rendered = renderEditorMetadataFormat(
|
|
132
|
+
"$model $model_id $model_name $provider $thinking $session_name literal",
|
|
133
|
+
values,
|
|
134
|
+
makeTheme(),
|
|
135
|
+
config,
|
|
136
|
+
);
|
|
137
|
+
expect(rendered).toContain("[success]Model Label");
|
|
138
|
+
expect(rendered).toContain("[success]model-id");
|
|
139
|
+
expect(rendered).toContain("[success]Model Name");
|
|
140
|
+
expect(rendered).toContain("[syntaxKeyword]Provider");
|
|
141
|
+
expect(rendered).toContain("[thinkingHigh]high");
|
|
142
|
+
expect(rendered).toContain("[border]Session");
|
|
143
|
+
expect(rendered).toContain("[border] literal");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("falls back to the shared thinking color", () => {
|
|
147
|
+
const config = {
|
|
148
|
+
...defaultConfig,
|
|
149
|
+
colors: { ...defaultConfig.colors, editorThinking: "thinkingText" },
|
|
150
|
+
};
|
|
151
|
+
expect(
|
|
152
|
+
renderEditorMetadataFormat("$thinking", { ...values, thinking: "low" }, makeTheme(), config),
|
|
153
|
+
).toBe("[thinkingText]low");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it.each([
|
|
157
|
+
[
|
|
158
|
+
{
|
|
159
|
+
editorThinking: "thinkingText",
|
|
160
|
+
editorThinkingXhigh: "thinkingXhigh",
|
|
161
|
+
editorThinkingMax: "thinkingMax",
|
|
162
|
+
},
|
|
163
|
+
"thinkingMax",
|
|
164
|
+
],
|
|
165
|
+
[{ editorThinking: "thinkingText", editorThinkingXhigh: "thinkingXhigh" }, "thinkingXhigh"],
|
|
166
|
+
[{ editorThinking: "thinkingText" }, "thinkingText"],
|
|
167
|
+
] as const)("resolves max thinking through its compatibility chain", (colors, expected) => {
|
|
168
|
+
const config = {
|
|
169
|
+
...defaultConfig,
|
|
170
|
+
colors: { ...defaultConfig.colors, ...colors },
|
|
171
|
+
};
|
|
172
|
+
expect(
|
|
173
|
+
renderEditorMetadataFormat("$thinking", { ...values, thinking: "max" }, makeTheme(), config),
|
|
174
|
+
).toBe(`[${expected}]max`);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it.each(["opencode", "opencode-copy-friendly"] as const)(
|
|
178
|
+
"plumbs usage metadata through the %s frame",
|
|
179
|
+
(style) => {
|
|
180
|
+
const config = structuredClone(defaultConfig);
|
|
181
|
+
config.components.editor.style = style;
|
|
182
|
+
config.components.editor.styles[style].metadataFormat = "$context $tokens $cache_hit";
|
|
183
|
+
const frame = renderPolishedEditorFrame({
|
|
184
|
+
width: 120,
|
|
185
|
+
editorLines: ["typed text"],
|
|
186
|
+
uiTheme: makeTheme(),
|
|
187
|
+
config,
|
|
188
|
+
modelMeta: {
|
|
189
|
+
modelLabel: values.model,
|
|
190
|
+
providerLabel: values.provider,
|
|
191
|
+
contextPercent: values.contextPercent,
|
|
192
|
+
contextWindow: values.contextWindow,
|
|
193
|
+
inputTokens: values.inputTokens,
|
|
194
|
+
outputTokens: values.outputTokens,
|
|
195
|
+
cacheHitRate: values.cacheHitRate,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const rendered = frame.join("\n");
|
|
199
|
+
expect(rendered).toContain("26.8%/272k");
|
|
200
|
+
expect(rendered).toContain("↑76k ↓1.6k");
|
|
201
|
+
expect(rendered).toContain("73.4%");
|
|
202
|
+
},
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
it("sanitizes literals and values without collapsing ordinary spaces", () => {
|
|
206
|
+
const rendered = render(
|
|
207
|
+
"\u001b]8;;https://example.com/$provider\u0007lit\u001b]8;;\u0007 x\n\ty:$model",
|
|
208
|
+
{
|
|
209
|
+
model: "\u001b]8;;https://example.com\u0007bad\u001b]8;;\u0007\r\n model\u0000",
|
|
210
|
+
},
|
|
211
|
+
);
|
|
212
|
+
expect(rendered).toBe("[border]lit x y:[accent]bad model");
|
|
213
|
+
expect(rendered).not.toMatch(/[\u0000-\u001f\u007f-\u009f]/);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe("editor metadata zones", () => {
|
|
218
|
+
it("keeps no-fill rendering byte-for-byte equivalent", () => {
|
|
219
|
+
const format = "$model( · $provider)(nested $fill)";
|
|
220
|
+
const legacy = renderEditorMetadataFormat(format, values, makeTheme(), defaultConfig);
|
|
221
|
+
const zones = renderEditorMetadataFormatSplit(format, values, makeTheme(), defaultConfig);
|
|
222
|
+
expect(zones).toEqual({ left: legacy, middle: "", right: "" });
|
|
223
|
+
expect(composeEditorMetadataLine(zones, undefined, 5)).toBe(legacy);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("keeps nested fill non-structural when a top-level fill creates zones", () => {
|
|
227
|
+
expect(
|
|
228
|
+
renderEditorMetadataFormatSplit(
|
|
229
|
+
"$model$fill(nested $fill)",
|
|
230
|
+
values,
|
|
231
|
+
makeTheme(),
|
|
232
|
+
defaultConfig,
|
|
233
|
+
),
|
|
234
|
+
).toEqual({ left: "[accent]Model Label", middle: "", right: "" });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("aligns one-fill left and right zones", () => {
|
|
238
|
+
expect(
|
|
239
|
+
composeEditorMetadataLine({ left: "left", middle: "", right: "right" }, undefined, 12),
|
|
240
|
+
).toBe("left right");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("centers the middle within the available gap", () => {
|
|
244
|
+
expect(
|
|
245
|
+
composeEditorMetadataLine({ left: "LLLLLLLLLL", middle: "MMM", right: "R" }, undefined, 20),
|
|
246
|
+
).toBe("LLLLLLLLLL MMM R");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("drops conditional empty zones and keeps content after extra fills on the right", () => {
|
|
250
|
+
const conditional = renderEditorMetadataFormatSplit(
|
|
251
|
+
"$model$fill( · $session_name)$fill($context)",
|
|
252
|
+
{ ...values, sessionName: "" },
|
|
253
|
+
makeTheme(),
|
|
254
|
+
defaultConfig,
|
|
255
|
+
);
|
|
256
|
+
expect(conditional).toEqual({
|
|
257
|
+
left: "[accent]Model Label",
|
|
258
|
+
middle: "",
|
|
259
|
+
right: "[border]26.8%/272k",
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const extra = renderEditorMetadataFormatSplit(
|
|
263
|
+
"$model$fill$provider$fill$thinking$fill$session_name",
|
|
264
|
+
values,
|
|
265
|
+
makeTheme(),
|
|
266
|
+
defaultConfig,
|
|
267
|
+
);
|
|
268
|
+
expect(extra).toEqual({
|
|
269
|
+
left: "[accent]Model Label",
|
|
270
|
+
middle: "[text]Provider",
|
|
271
|
+
right: "[muted]high[border]Session",
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("omits an unfittable middle instead of truncating it", () => {
|
|
276
|
+
const line = composeEditorMetadataLine(
|
|
277
|
+
{ left: "left", middle: "middle", right: "right" },
|
|
278
|
+
undefined,
|
|
279
|
+
11,
|
|
280
|
+
);
|
|
281
|
+
expect(line).toBe("left right");
|
|
282
|
+
expect(line).not.toContain("middle");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("reserves operational right status, then configured left, then configured right", () => {
|
|
286
|
+
const wide = composeEditorMetadataLine(
|
|
287
|
+
{ left: "left", middle: "", right: "right" },
|
|
288
|
+
"INSERT",
|
|
289
|
+
24,
|
|
290
|
+
);
|
|
291
|
+
expect(visibleWidth(wide)).toBe(24);
|
|
292
|
+
expect(wide.endsWith("right INSERT")).toBe(true);
|
|
293
|
+
|
|
294
|
+
expect(
|
|
295
|
+
stripVTControlCharacters(
|
|
296
|
+
composeEditorMetadataLine(
|
|
297
|
+
{ left: "left-long", middle: "middle", right: "configured-right" },
|
|
298
|
+
"INSERT",
|
|
299
|
+
12,
|
|
300
|
+
),
|
|
301
|
+
),
|
|
302
|
+
).toBe("left- INSERT");
|
|
303
|
+
expect(
|
|
304
|
+
stripVTControlCharacters(
|
|
305
|
+
composeEditorMetadataLine({ left: "left", middle: "", right: "right" }, "INSERT", 4),
|
|
306
|
+
),
|
|
307
|
+
).toBe("INSE");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("keeps ANSI, CJK, combining marks, and emoji within the cell budget", () => {
|
|
311
|
+
const line = composeEditorMetadataLine(
|
|
312
|
+
{
|
|
313
|
+
left: "\x1b[31m界e\u0301👩💻abcdef\x1b[0m",
|
|
314
|
+
middle: "中🙂",
|
|
315
|
+
right: "右🙂tail",
|
|
316
|
+
},
|
|
317
|
+
"INSERT",
|
|
318
|
+
22,
|
|
319
|
+
);
|
|
320
|
+
expect(visibleWidth(line)).toBeLessThanOrEqual(22);
|
|
321
|
+
const plain = stripVTControlCharacters(line);
|
|
322
|
+
expect(plain).toContain("界e\u0301👩💻");
|
|
323
|
+
expect(plain).toContain("INSERT");
|
|
324
|
+
expect(plain).not.toContain("�");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it.each([
|
|
328
|
+
["opencode", 2, "regular-left", "regular-right"],
|
|
329
|
+
["opencode-copy-friendly", 1, "copy-left", "copy-right"],
|
|
330
|
+
] as const)(
|
|
331
|
+
"uses the %s metadata format and its chrome budget",
|
|
332
|
+
(style, chromeWidth, expectedLeft, expectedRight) => {
|
|
333
|
+
const config = structuredClone(defaultConfig);
|
|
334
|
+
config.components.editor.style = style;
|
|
335
|
+
config.components.editor.styles.opencode.metadataFormat =
|
|
336
|
+
"regular-left$" + "{fill}regular-right";
|
|
337
|
+
config.components.editor.styles["opencode-copy-friendly"].metadataFormat =
|
|
338
|
+
"copy-left$" + "{fill}copy-right";
|
|
339
|
+
const width = 40;
|
|
340
|
+
const frame = renderPolishedEditorFrame({
|
|
341
|
+
width,
|
|
342
|
+
editorLines: ["draft"],
|
|
343
|
+
uiTheme: makeAnsiTheme(),
|
|
344
|
+
config,
|
|
345
|
+
modelMeta: { modelLabel: "model", providerLabel: "provider" },
|
|
346
|
+
});
|
|
347
|
+
const metadata = frame.find(
|
|
348
|
+
(line) => line.includes(expectedLeft) && line.includes(expectedRight),
|
|
349
|
+
);
|
|
350
|
+
expect(metadata).toBeDefined();
|
|
351
|
+
expect(visibleWidth(metadata ?? "")).toBe(width);
|
|
352
|
+
const plain = stripVTControlCharacters(metadata ?? "");
|
|
353
|
+
expect(plain.indexOf(expectedLeft)).toBe(chromeWidth);
|
|
354
|
+
expect(plain.endsWith(expectedRight)).toBe(true);
|
|
355
|
+
},
|
|
356
|
+
);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("sanitizeEditorMetadataText", () => {
|
|
360
|
+
it("removes SGR and BEL while preserving regular spaces", () => {
|
|
361
|
+
expect(sanitizeEditorMetadataText("a b\r\n\tc\u0000\u0007\u001b[31md\u001b[0m")).toBe(
|
|
362
|
+
"a b cd",
|
|
363
|
+
);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("preserves hyperlink labels between ST-terminated OSC 8 sequences", () => {
|
|
367
|
+
expect(
|
|
368
|
+
sanitizeEditorMetadataText(
|
|
369
|
+
"before \u001b]8;;https://example.com\u001b\\visible label\u001b]8;;\u001b\\ after",
|
|
370
|
+
),
|
|
371
|
+
).toBe("before visible label after");
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it("removes C1 OSC, DCS, and CSI payloads", () => {
|
|
375
|
+
expect(
|
|
376
|
+
sanitizeEditorMetadataText(
|
|
377
|
+
"a\u009d8;;https://example.com\u009cb\u0090hidden payload\u009cc\u009b31md",
|
|
378
|
+
),
|
|
379
|
+
).toBe("abcd");
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it("resumes after CAN or SUB cancels ESC-prefixed control sequences", () => {
|
|
383
|
+
expect(sanitizeEditorMetadataText("safe\u001b]8;;url\u0018 visible")).toBe("safe visible");
|
|
384
|
+
expect(sanitizeEditorMetadataText("safe\u001bPpayload\u001a visible")).toBe("safe visible");
|
|
385
|
+
expect(sanitizeEditorMetadataText("safe\u001b[31\u0018 visible")).toBe("safe visible");
|
|
386
|
+
expect(sanitizeEditorMetadataText("safe\u001b[31\u001a visible")).toBe("safe visible");
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("resumes after CAN or SUB cancels C1 control sequences", () => {
|
|
390
|
+
expect(sanitizeEditorMetadataText("safe\u009d8;;url\u001a visible")).toBe("safe visible");
|
|
391
|
+
expect(sanitizeEditorMetadataText("safe\u0090payload\u0018 visible")).toBe("safe visible");
|
|
392
|
+
expect(sanitizeEditorMetadataText("safe\u009b31\u0018 visible")).toBe("safe visible");
|
|
393
|
+
expect(sanitizeEditorMetadataText("safe\u009b31\u001a visible")).toBe("safe visible");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it("drops unterminated control sequences conservatively", () => {
|
|
397
|
+
expect(sanitizeEditorMetadataText("safe\u001b]8;;https://example.com/unsafe")).toBe("safe");
|
|
398
|
+
expect(sanitizeEditorMetadataText("safe\u0090unsafe payload")).toBe("safe");
|
|
399
|
+
expect(sanitizeEditorMetadataText("safe\u009b31")).toBe("safe");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("normalizes NEL, line separators, and paragraph separators", () => {
|
|
403
|
+
expect(sanitizeEditorMetadataText("a\u0085\u2028\u2029b c")).toBe("a b c");
|
|
404
|
+
});
|
|
405
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { replaceEditorComponentWithExpandedText } from "../extensions/zentui/editor-transfer";
|
|
3
|
+
|
|
4
|
+
type Editor = {
|
|
5
|
+
getText(): string;
|
|
6
|
+
setText(text: string): void;
|
|
7
|
+
getExpandedText?: () => string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type Factory = () => Editor;
|
|
11
|
+
|
|
12
|
+
function piLikeUi(initialEditor: Editor, operations: string[], initialFactory?: Factory) {
|
|
13
|
+
let active = initialEditor;
|
|
14
|
+
let configuredFactory = initialFactory;
|
|
15
|
+
return {
|
|
16
|
+
get active() {
|
|
17
|
+
return active;
|
|
18
|
+
},
|
|
19
|
+
get configuredFactory() {
|
|
20
|
+
return configuredFactory;
|
|
21
|
+
},
|
|
22
|
+
getEditorComponent() {
|
|
23
|
+
operations.push("getEditorComponent");
|
|
24
|
+
return configuredFactory;
|
|
25
|
+
},
|
|
26
|
+
getEditorText() {
|
|
27
|
+
operations.push("getEditorText");
|
|
28
|
+
return active.getExpandedText?.() ?? active.getText();
|
|
29
|
+
},
|
|
30
|
+
setEditorText(text: string) {
|
|
31
|
+
operations.push(`setEditorText:${text}`);
|
|
32
|
+
active.setText(text);
|
|
33
|
+
},
|
|
34
|
+
setEditorComponent(factory: Factory | undefined) {
|
|
35
|
+
operations.push("setEditorComponent");
|
|
36
|
+
configuredFactory = factory;
|
|
37
|
+
const currentText = active.getText();
|
|
38
|
+
if (!factory) return;
|
|
39
|
+
const next = factory();
|
|
40
|
+
next.setText(currentText);
|
|
41
|
+
active = next;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function textEditor(initial = ""): Editor {
|
|
47
|
+
let text = initial;
|
|
48
|
+
return {
|
|
49
|
+
getText: () => text,
|
|
50
|
+
setText(next) {
|
|
51
|
+
text = next;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("safe editor component transfer", () => {
|
|
57
|
+
it("expands a collapsed paste before Pi snapshots the replaced editor", () => {
|
|
58
|
+
const marker = "[paste #1 +12 lines]";
|
|
59
|
+
const expanded = Array.from({ length: 12 }, (_, index) => `line ${index + 1}`).join("\n");
|
|
60
|
+
let oldText = marker;
|
|
61
|
+
const operations: string[] = [];
|
|
62
|
+
const oldEditor: Editor = {
|
|
63
|
+
getText: () => oldText,
|
|
64
|
+
getExpandedText: () => (oldText === marker ? expanded : oldText),
|
|
65
|
+
setText(text) {
|
|
66
|
+
oldText = text;
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const ui = piLikeUi(oldEditor, operations);
|
|
70
|
+
const next = textEditor();
|
|
71
|
+
|
|
72
|
+
const result = replaceEditorComponentWithExpandedText(ui, () => next);
|
|
73
|
+
|
|
74
|
+
expect(result).toEqual({ ok: true });
|
|
75
|
+
expect(operations).toEqual([
|
|
76
|
+
"getEditorComponent",
|
|
77
|
+
"getEditorText",
|
|
78
|
+
`setEditorText:${expanded}`,
|
|
79
|
+
"setEditorComponent",
|
|
80
|
+
]);
|
|
81
|
+
expect(ui.active).toBe(next);
|
|
82
|
+
expect(next.getText()).toBe(expanded);
|
|
83
|
+
expect(next.getText()).not.toContain(marker);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it.each(["ordinary prompt", ""])("transfers normal prompt text: %j", (text) => {
|
|
87
|
+
const operations: string[] = [];
|
|
88
|
+
const ui = piLikeUi(textEditor(text), operations);
|
|
89
|
+
const next = textEditor();
|
|
90
|
+
|
|
91
|
+
expect(replaceEditorComponentWithExpandedText(ui, () => next)).toEqual({ ok: true });
|
|
92
|
+
expect(next.getText()).toBe(text);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("uses the public getText fallback for a third-party editor without getExpandedText", () => {
|
|
96
|
+
const operations: string[] = [];
|
|
97
|
+
const ui = piLikeUi(textEditor("third-party draft"), operations);
|
|
98
|
+
const next = textEditor();
|
|
99
|
+
|
|
100
|
+
expect(replaceEditorComponentWithExpandedText(ui, () => next)).toEqual({ ok: true });
|
|
101
|
+
expect(next.getText()).toBe("third-party draft");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("does not replace when the public transfer API is unavailable", () => {
|
|
105
|
+
const setEditorComponent = vi.fn();
|
|
106
|
+
expect(replaceEditorComponentWithExpandedText({ setEditorComponent }, undefined)).toEqual({
|
|
107
|
+
ok: false,
|
|
108
|
+
reason: "unsupported-transfer-api",
|
|
109
|
+
});
|
|
110
|
+
expect(setEditorComponent).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("does not replace when the previous factory cannot be snapshotted", () => {
|
|
114
|
+
const getEditorText = vi.fn();
|
|
115
|
+
const setEditorComponent = vi.fn();
|
|
116
|
+
const result = replaceEditorComponentWithExpandedText(
|
|
117
|
+
{
|
|
118
|
+
getEditorComponent() {
|
|
119
|
+
throw new Error("factory snapshot failed");
|
|
120
|
+
},
|
|
121
|
+
getEditorText,
|
|
122
|
+
setEditorText() {},
|
|
123
|
+
setEditorComponent,
|
|
124
|
+
},
|
|
125
|
+
undefined,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(result).toEqual({ ok: false, reason: "editor-factory-snapshot-failed" });
|
|
129
|
+
expect(getEditorText).not.toHaveBeenCalled();
|
|
130
|
+
expect(setEditorComponent).not.toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("does not replace when expanded text snapshotting fails", () => {
|
|
134
|
+
const setEditorText = vi.fn();
|
|
135
|
+
const setEditorComponent = vi.fn();
|
|
136
|
+
const result = replaceEditorComponentWithExpandedText(
|
|
137
|
+
{
|
|
138
|
+
getEditorComponent: () => undefined,
|
|
139
|
+
getEditorText() {
|
|
140
|
+
throw new Error("snapshot failed");
|
|
141
|
+
},
|
|
142
|
+
setEditorText,
|
|
143
|
+
setEditorComponent,
|
|
144
|
+
},
|
|
145
|
+
undefined,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
expect(result).toEqual({ ok: false, reason: "editor-text-snapshot-failed" });
|
|
149
|
+
expect(setEditorText).not.toHaveBeenCalled();
|
|
150
|
+
expect(setEditorComponent).not.toHaveBeenCalled();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("rejects a non-string snapshot without replacing", () => {
|
|
154
|
+
const setEditorComponent = vi.fn();
|
|
155
|
+
const result = replaceEditorComponentWithExpandedText(
|
|
156
|
+
{
|
|
157
|
+
getEditorComponent: () => undefined,
|
|
158
|
+
getEditorText: () => undefined,
|
|
159
|
+
setEditorText() {},
|
|
160
|
+
setEditorComponent,
|
|
161
|
+
},
|
|
162
|
+
undefined,
|
|
163
|
+
);
|
|
164
|
+
expect(result).toEqual({ ok: false, reason: "editor-text-snapshot-failed" });
|
|
165
|
+
expect(setEditorComponent).not.toHaveBeenCalled();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("does not replace when writing expanded text back fails", () => {
|
|
169
|
+
const setEditorComponent = vi.fn();
|
|
170
|
+
const result = replaceEditorComponentWithExpandedText(
|
|
171
|
+
{
|
|
172
|
+
getEditorComponent: () => undefined,
|
|
173
|
+
getEditorText: () => "expanded",
|
|
174
|
+
setEditorText() {
|
|
175
|
+
throw new Error("write failed");
|
|
176
|
+
},
|
|
177
|
+
setEditorComponent,
|
|
178
|
+
},
|
|
179
|
+
undefined,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
expect(result).toEqual({ ok: false, reason: "editor-text-preparation-failed" });
|
|
183
|
+
expect(setEditorComponent).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("restores the previous factory after an assignment-first replacement failure", () => {
|
|
187
|
+
const operations: string[] = [];
|
|
188
|
+
const previousEditor = textEditor("marker");
|
|
189
|
+
const previousFactory = () => previousEditor;
|
|
190
|
+
const ui = piLikeUi(previousEditor, operations, previousFactory);
|
|
191
|
+
const failingFactory = () => {
|
|
192
|
+
throw new Error("replacement failed after assignment");
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const result = replaceEditorComponentWithExpandedText(ui, failingFactory);
|
|
196
|
+
|
|
197
|
+
expect(result).toEqual({
|
|
198
|
+
ok: false,
|
|
199
|
+
reason: "editor-replacement-failed-with-rollback",
|
|
200
|
+
});
|
|
201
|
+
expect(ui.configuredFactory).toBe(previousFactory);
|
|
202
|
+
expect(ui.active.getText()).toBe("marker");
|
|
203
|
+
expect(operations).toEqual([
|
|
204
|
+
"getEditorComponent",
|
|
205
|
+
"getEditorText",
|
|
206
|
+
"setEditorText:marker",
|
|
207
|
+
"setEditorComponent",
|
|
208
|
+
"setEditorComponent",
|
|
209
|
+
]);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("reports when assignment-first replacement rollback also throws", () => {
|
|
213
|
+
const previousFactory = () => textEditor("previous");
|
|
214
|
+
let configuredFactory: Factory | undefined = previousFactory;
|
|
215
|
+
let setterCalls = 0;
|
|
216
|
+
const result = replaceEditorComponentWithExpandedText(
|
|
217
|
+
{
|
|
218
|
+
getEditorComponent: () => configuredFactory,
|
|
219
|
+
getEditorText: () => "expanded payload",
|
|
220
|
+
setEditorText() {},
|
|
221
|
+
setEditorComponent(factory) {
|
|
222
|
+
configuredFactory = factory;
|
|
223
|
+
setterCalls += 1;
|
|
224
|
+
throw new Error(setterCalls === 1 ? "replacement failed" : "rollback failed");
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
() => textEditor("next"),
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
expect(result).toEqual({
|
|
231
|
+
ok: false,
|
|
232
|
+
reason: "editor-replacement-rollback-failed",
|
|
233
|
+
});
|
|
234
|
+
expect(setterCalls).toBe(2);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("submits the exact original multiline paste payload after replacement", () => {
|
|
238
|
+
const marker = "[paste #7 +4 lines]";
|
|
239
|
+
const payload = "alpha\nbeta\ngamma\ndelta";
|
|
240
|
+
let oldText = marker;
|
|
241
|
+
const submitted: string[] = [];
|
|
242
|
+
const oldEditor: Editor = {
|
|
243
|
+
getText: () => oldText,
|
|
244
|
+
getExpandedText: () => (oldText === marker ? payload : oldText),
|
|
245
|
+
setText(text) {
|
|
246
|
+
oldText = text;
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
const operations: string[] = [];
|
|
250
|
+
const ui = piLikeUi(oldEditor, operations);
|
|
251
|
+
const next = textEditor();
|
|
252
|
+
|
|
253
|
+
expect(replaceEditorComponentWithExpandedText(ui, () => next)).toEqual({ ok: true });
|
|
254
|
+
const onSubmit = (text: string) => submitted.push(text);
|
|
255
|
+
onSubmit(next.getText());
|
|
256
|
+
|
|
257
|
+
expect(submitted).toEqual([payload]);
|
|
258
|
+
expect(submitted[0]).not.toContain("[paste #");
|
|
259
|
+
});
|
|
260
|
+
});
|