@selesai/code 0.9.14 → 0.10.0
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 +79 -29
- package/dist/defaults/settings.json +3 -7
- package/dist/extensions/package.json +3 -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/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/docs/plans/auto-model-routing-extension.md +490 -0
- package/package.json +3 -3
- 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
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
-
import { SessionManager, type ExtensionAPI, type ExtensionCommandContext } from "@selesai/code";
|
|
5
|
-
import type { AutocompleteItem } from "@earendil-works/pi-tui";
|
|
6
|
-
|
|
7
|
-
function stripMatchingQuotes(value: string): string {
|
|
8
|
-
const trimmed = value.trim();
|
|
9
|
-
if (trimmed.length >= 2) {
|
|
10
|
-
const first = trimmed[0];
|
|
11
|
-
const last = trimmed[trimmed.length - 1];
|
|
12
|
-
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
13
|
-
return trimmed.slice(1, -1);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
return trimmed;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function unescapePathArg(value: string): string {
|
|
20
|
-
return stripMatchingQuotes(value).replace(/\\(.)/g, "$1");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function expandHomePath(value: string, homeDir = homedir()): string {
|
|
24
|
-
if (value === "~") return homeDir;
|
|
25
|
-
if (value.startsWith("~/")) return join(homeDir, value.slice(2));
|
|
26
|
-
return value;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function escapeCompletionPath(value: string): string {
|
|
30
|
-
return value.replace(/([\\\s"'`$&|;<>()[\]{}?!*])/g, "\\$1");
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function displayPath(value: string): string {
|
|
34
|
-
return value.replace(/\\/g, "/");
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function resolveCdTarget(args: string, cwd: string, homeDir = homedir()): { path: string } | { error: string } | null {
|
|
38
|
-
const input = unescapePathArg(args);
|
|
39
|
-
if (!input) return null;
|
|
40
|
-
|
|
41
|
-
const expanded = expandHomePath(input, homeDir);
|
|
42
|
-
const target = isAbsolute(expanded) ? resolve(expanded) : resolve(cwd, expanded);
|
|
43
|
-
|
|
44
|
-
if (!existsSync(target)) {
|
|
45
|
-
return { error: `Directory does not exist: ${target}` };
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
try {
|
|
49
|
-
if (!statSync(target).isDirectory()) {
|
|
50
|
-
return { error: `Not a directory: ${target}` };
|
|
51
|
-
}
|
|
52
|
-
} catch (error) {
|
|
53
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
-
return { error: `Cannot access directory: ${message}` };
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return { path: target };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function pathBase(token: string, cwd: string, homeDir: string): { dir: string; prefix: string; displayPrefix: string } {
|
|
61
|
-
const unescaped = unescapePathArg(token);
|
|
62
|
-
const expanded = expandHomePath(unescaped, homeDir);
|
|
63
|
-
const hasSlash = expanded.includes("/");
|
|
64
|
-
|
|
65
|
-
if (!hasSlash) {
|
|
66
|
-
return { dir: cwd, prefix: expanded, displayPrefix: "" };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const baseDir = expanded.endsWith("/") ? expanded.slice(0, -1) : dirname(expanded);
|
|
70
|
-
const dir = isAbsolute(baseDir) ? baseDir : resolve(cwd, baseDir);
|
|
71
|
-
const prefix = expanded.endsWith("/") ? "" : basename(expanded);
|
|
72
|
-
const displayPrefix = unescaped.endsWith("/") ? unescaped : unescaped.slice(0, Math.max(0, unescaped.length - prefix.length));
|
|
73
|
-
return { dir, prefix, displayPrefix };
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function isDirectoryEntry(dir: string, name: string): boolean {
|
|
77
|
-
try {
|
|
78
|
-
return statSync(join(dir, name)).isDirectory();
|
|
79
|
-
} catch {
|
|
80
|
-
return false;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function getCdArgumentCompletions(argumentPrefix: string, cwd: string, homeDir = homedir()): AutocompleteItem[] {
|
|
85
|
-
const prefix = argumentPrefix.trimStart();
|
|
86
|
-
const suggestions: AutocompleteItem[] = [];
|
|
87
|
-
|
|
88
|
-
for (const quickPick of ["../", "~/"]) {
|
|
89
|
-
if (!prefix || quickPick.startsWith(prefix)) {
|
|
90
|
-
suggestions.push({ value: quickPick, label: quickPick, description: quickPick === "../" ? "Parent directory" : "Home directory" });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const { dir, prefix: entryPrefix, displayPrefix } = pathBase(prefix, cwd, homeDir);
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
const entries = readdirSync(dir, { withFileTypes: true })
|
|
98
|
-
.filter((entry) => entry.name.startsWith(entryPrefix) && (entry.isDirectory() || entry.isSymbolicLink() && isDirectoryEntry(dir, entry.name)))
|
|
99
|
-
.slice(0, 50)
|
|
100
|
-
.map((entry) => {
|
|
101
|
-
const path = displayPath(`${displayPrefix}${entry.name}/`);
|
|
102
|
-
return {
|
|
103
|
-
value: escapeCompletionPath(path),
|
|
104
|
-
label: path,
|
|
105
|
-
} satisfies AutocompleteItem;
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
suggestions.push(...entries);
|
|
109
|
-
} catch {
|
|
110
|
-
return suggestions;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const seen = new Set<string>();
|
|
114
|
-
return suggestions.filter((item) => {
|
|
115
|
-
if (seen.has(item.value)) return false;
|
|
116
|
-
seen.add(item.value);
|
|
117
|
-
return true;
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function removeSessionFile(filePath: string | undefined): void {
|
|
122
|
-
if (!filePath) return;
|
|
123
|
-
try {
|
|
124
|
-
unlinkSync(filePath);
|
|
125
|
-
} catch {
|
|
126
|
-
// Best-effort cleanup only; the command still reports the cancellation/error.
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
export async function runCdCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
131
|
-
const resolved = resolveCdTarget(args, ctx.cwd);
|
|
132
|
-
if (resolved === null) {
|
|
133
|
-
ctx.ui.notify(ctx.cwd, "info");
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if ("error" in resolved) {
|
|
137
|
-
ctx.ui.notify(resolved.error, "error");
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (resolved.path === ctx.cwd) {
|
|
141
|
-
ctx.ui.notify(`Already in ${ctx.cwd}`, "info");
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
146
|
-
if (!sessionFile) {
|
|
147
|
-
ctx.ui.notify("/cd requires a persisted Pi session", "error");
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
await ctx.waitForIdle();
|
|
152
|
-
const nextSession = SessionManager.forkFrom(sessionFile, resolved.path);
|
|
153
|
-
const nextSessionFile = nextSession.getSessionFile();
|
|
154
|
-
|
|
155
|
-
try {
|
|
156
|
-
const result = await ctx.switchSession(nextSessionFile!, {
|
|
157
|
-
withSession: async (nextCtx) => {
|
|
158
|
-
try {
|
|
159
|
-
process.chdir(nextCtx.cwd);
|
|
160
|
-
nextCtx.ui.notify(`Changed directory to ${nextCtx.cwd}`, "info");
|
|
161
|
-
nextCtx.ui.setTitle(`pi - ${basename(nextCtx.cwd) || nextCtx.cwd}`);
|
|
162
|
-
} catch (error) {
|
|
163
|
-
console.debug("[powerline-footer] Failed to update process cwd after /cd:", error);
|
|
164
|
-
}
|
|
165
|
-
},
|
|
166
|
-
});
|
|
167
|
-
if (result.cancelled) {
|
|
168
|
-
removeSessionFile(nextSessionFile);
|
|
169
|
-
ctx.ui.notify("Directory change cancelled", "warning");
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
} catch (error) {
|
|
173
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
174
|
-
try {
|
|
175
|
-
ctx.ui.notify(`Failed to change directory: ${message}`, "error");
|
|
176
|
-
} catch {
|
|
177
|
-
console.debug("[powerline-footer] Failed to report /cd error:", error);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function registerCdCommand(pi: ExtensionAPI, getCwd: () => string): void {
|
|
183
|
-
pi.registerCommand("cd", {
|
|
184
|
-
description: "Switch the Pi session working directory",
|
|
185
|
-
getArgumentCompletions(argumentPrefix) {
|
|
186
|
-
return getCdArgumentCompletions(argumentPrefix, getCwd());
|
|
187
|
-
},
|
|
188
|
-
handler: runCdCommand,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
// ANSI escape codes for colors
|
|
2
|
-
// Matching oh-my-pi dark theme colors exactly
|
|
3
|
-
|
|
4
|
-
export interface AnsiColors {
|
|
5
|
-
getBgAnsi(r: number, g: number, b: number): string;
|
|
6
|
-
getFgAnsi(r: number, g: number, b: number): string;
|
|
7
|
-
getFgAnsi256(code: number): string;
|
|
8
|
-
reset: string;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const ansi: AnsiColors = {
|
|
12
|
-
getBgAnsi: (r, g, b) => `\x1b[48;2;${r};${g};${b}m`,
|
|
13
|
-
getFgAnsi: (r, g, b) => `\x1b[38;2;${r};${g};${b}m`,
|
|
14
|
-
getFgAnsi256: (code) => `\x1b[38;5;${code}m`,
|
|
15
|
-
reset: "\x1b[0m",
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// Convert hex to RGB tuple
|
|
19
|
-
function hexToRgb(hex: string): [number, number, number] {
|
|
20
|
-
const h = hex.replace("#", "");
|
|
21
|
-
return [
|
|
22
|
-
parseInt(h.slice(0, 2), 16),
|
|
23
|
-
parseInt(h.slice(2, 4), 16),
|
|
24
|
-
parseInt(h.slice(4, 6), 16),
|
|
25
|
-
];
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Colors used by welcome/editor chrome rendering
|
|
29
|
-
const THEME = {
|
|
30
|
-
sep: 244, // ANSI 256 gray
|
|
31
|
-
model: "#d787af", // Pink/mauve
|
|
32
|
-
path: "#00afaf", // Teal/cyan
|
|
33
|
-
gitClean: "#5faf5f", // Green
|
|
34
|
-
accent: "#febc38", // Orange
|
|
35
|
-
queue: "#febc38", // Orange
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// Color name to ANSI code mapping
|
|
39
|
-
type ColorName = keyof typeof THEME;
|
|
40
|
-
|
|
41
|
-
function getAnsiCode(color: ColorName): string {
|
|
42
|
-
const value = THEME[color as keyof typeof THEME];
|
|
43
|
-
|
|
44
|
-
if (value === undefined || value === "") {
|
|
45
|
-
return ""; // No color, use terminal default
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
if (typeof value === "number") {
|
|
49
|
-
return ansi.getFgAnsi256(value);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (typeof value === "string" && value.startsWith("#")) {
|
|
53
|
-
const [r, g, b] = hexToRgb(value);
|
|
54
|
-
return ansi.getFgAnsi(r, g, b);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return "";
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Helper to apply foreground color only (no reset - caller manages reset)
|
|
61
|
-
export function fgOnly(color: ColorName, text: string): string {
|
|
62
|
-
const code = getAnsiCode(color);
|
|
63
|
-
return code ? `${code}${text}` : text;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Get raw ANSI code for a color
|
|
67
|
-
export function getFgAnsiCode(color: ColorName): string {
|
|
68
|
-
return getAnsiCode(color);
|
|
69
|
-
}
|
|
70
|
-
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { estimateTokens, sessionEntryToContextMessages, type SessionEntry } from "@selesai/code";
|
|
2
|
-
|
|
3
|
-
export interface CoreContextUsage {
|
|
4
|
-
contextTokens: number | null;
|
|
5
|
-
contextWindow: number;
|
|
6
|
-
contextPercent: number | null;
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
interface DisplayContextUsageInput {
|
|
10
|
-
coreContextUsage: CoreContextUsage | null;
|
|
11
|
-
unknownCoreFallback: CoreContextUsage | null;
|
|
12
|
-
fallbackContextTokens: number;
|
|
13
|
-
fallbackContextWindow: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
17
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface ContextUsageSource {
|
|
21
|
-
sessionManager: {
|
|
22
|
-
getLeafId(): string | null;
|
|
23
|
-
};
|
|
24
|
-
getContextUsage(): unknown;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface UnknownContextEstimateSource {
|
|
28
|
-
sessionManager: {
|
|
29
|
-
buildContextEntries(): SessionEntry[];
|
|
30
|
-
};
|
|
31
|
-
getContextUsage(): unknown;
|
|
32
|
-
getSystemPrompt(): string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isContextUsageSource(value: unknown): value is ContextUsageSource {
|
|
36
|
-
if (!isRecord(value) || typeof value.getContextUsage !== "function" || !isRecord(value.sessionManager)) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
return typeof value.sessionManager.getLeafId === "function";
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function isUnknownContextEstimateSource(value: unknown): value is UnknownContextEstimateSource {
|
|
43
|
-
return isRecord(value)
|
|
44
|
-
&& typeof value.getContextUsage === "function"
|
|
45
|
-
&& typeof value.getSystemPrompt === "function"
|
|
46
|
-
&& isRecord(value.sessionManager)
|
|
47
|
-
&& typeof value.sessionManager.buildContextEntries === "function";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export class CoreContextUsageCache {
|
|
51
|
-
private sessionManager: ContextUsageSource["sessionManager"] | null = null;
|
|
52
|
-
private leafId: string | null = null;
|
|
53
|
-
private usage: CoreContextUsage | null = null;
|
|
54
|
-
|
|
55
|
-
get(ctx: unknown): CoreContextUsage | null {
|
|
56
|
-
if (!isContextUsageSource(ctx)) return readCoreContextUsage(ctx);
|
|
57
|
-
|
|
58
|
-
const sessionManager = ctx.sessionManager;
|
|
59
|
-
const leafId = sessionManager.getLeafId();
|
|
60
|
-
// A reset cache holds sessionManager === null, which never matches a live one.
|
|
61
|
-
if (this.sessionManager !== sessionManager || this.leafId !== leafId) {
|
|
62
|
-
this.sessionManager = sessionManager;
|
|
63
|
-
this.leafId = leafId;
|
|
64
|
-
this.usage = readCoreContextUsage(ctx);
|
|
65
|
-
}
|
|
66
|
-
return this.usage;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
reset(): void {
|
|
70
|
-
this.sessionManager = null;
|
|
71
|
-
this.leafId = null;
|
|
72
|
-
this.usage = null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function estimateInitialContextTokens(ctx: unknown): number | null {
|
|
77
|
-
if (!isRecord(ctx) || typeof ctx.getSystemPrompt !== "function") {
|
|
78
|
-
return null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const prompt = ctx.getSystemPrompt();
|
|
82
|
-
if (typeof prompt !== "string" || !prompt.trim()) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
return Math.ceil(prompt.length / 4);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function resolveDisplayContextUsage({
|
|
90
|
-
coreContextUsage,
|
|
91
|
-
unknownCoreFallback,
|
|
92
|
-
fallbackContextTokens,
|
|
93
|
-
fallbackContextWindow,
|
|
94
|
-
}: DisplayContextUsageInput): CoreContextUsage {
|
|
95
|
-
if (coreContextUsage?.contextTokens === null && unknownCoreFallback) return unknownCoreFallback;
|
|
96
|
-
if (coreContextUsage) return coreContextUsage;
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
contextTokens: fallbackContextTokens,
|
|
100
|
-
contextWindow: fallbackContextWindow,
|
|
101
|
-
contextPercent: fallbackContextWindow > 0 ? (fallbackContextTokens / fallbackContextWindow) * 100 : 0,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function estimateUnknownContextUsage(ctx: unknown): CoreContextUsage | null {
|
|
106
|
-
if (!isUnknownContextEstimateSource(ctx)) return null;
|
|
107
|
-
|
|
108
|
-
const coreContextUsage = readCoreContextUsage(ctx);
|
|
109
|
-
if (!coreContextUsage || coreContextUsage.contextTokens !== null) return null;
|
|
110
|
-
|
|
111
|
-
const messageTokens = ctx.sessionManager.buildContextEntries()
|
|
112
|
-
.flatMap(sessionEntryToContextMessages)
|
|
113
|
-
.reduce((total, message) => total + estimateTokens(message), 0);
|
|
114
|
-
const contextTokens = (estimateInitialContextTokens(ctx) ?? 0) + messageTokens;
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
contextTokens,
|
|
118
|
-
contextWindow: coreContextUsage.contextWindow,
|
|
119
|
-
contextPercent: (contextTokens / coreContextUsage.contextWindow) * 100,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
export function readCoreContextUsage(ctx: unknown): CoreContextUsage | null {
|
|
124
|
-
if (!isRecord(ctx) || typeof ctx.getContextUsage !== "function") {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const usage = ctx.getContextUsage();
|
|
129
|
-
if (!isRecord(usage)) {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const tokens = usage.tokens;
|
|
134
|
-
const contextWindow = usage.contextWindow;
|
|
135
|
-
if (
|
|
136
|
-
!(tokens === null || (typeof tokens === "number" && Number.isFinite(tokens)))
|
|
137
|
-
|| typeof contextWindow !== "number"
|
|
138
|
-
|| !Number.isFinite(contextWindow)
|
|
139
|
-
|| contextWindow <= 0
|
|
140
|
-
) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (tokens === null) {
|
|
145
|
-
return { contextTokens: null, contextWindow, contextPercent: null };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const percent = usage.percent;
|
|
149
|
-
return {
|
|
150
|
-
contextTokens: tokens,
|
|
151
|
-
contextWindow,
|
|
152
|
-
contextPercent: typeof percent === "number" && Number.isFinite(percent)
|
|
153
|
-
? percent
|
|
154
|
-
: (tokens / contextWindow) * 100,
|
|
155
|
-
};
|
|
156
|
-
}
|
|
@@ -1,142 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname } from "node:path";
|
|
3
|
-
import { getAgentPath } from "./paths.ts";
|
|
4
|
-
|
|
5
|
-
export const SUPPORTED_COST_CURRENCIES = ["USD", "CNY", "EUR", "GBP", "JPY", "CAD", "AUD", "CHF", "INR", "KRW"] as const;
|
|
6
|
-
export type CostCurrencyCode = typeof SUPPORTED_COST_CURRENCIES[number];
|
|
7
|
-
|
|
8
|
-
const CURRENCY_SYMBOLS: Record<CostCurrencyCode, string> = {
|
|
9
|
-
USD: "$",
|
|
10
|
-
CNY: "¥",
|
|
11
|
-
EUR: "€",
|
|
12
|
-
GBP: "£",
|
|
13
|
-
JPY: "¥",
|
|
14
|
-
CAD: "CA$",
|
|
15
|
-
AUD: "A$",
|
|
16
|
-
CHF: "CHF ",
|
|
17
|
-
INR: "₹",
|
|
18
|
-
KRW: "₩",
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
const RATE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
22
|
-
const RATE_URL = "https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/usd.min.json";
|
|
23
|
-
const RATE_CACHE_PATH = getAgentPath("powerline-footer", "currency-rates.json");
|
|
24
|
-
|
|
25
|
-
type RateTable = Partial<Record<CostCurrencyCode, number>>;
|
|
26
|
-
|
|
27
|
-
interface CachedRates {
|
|
28
|
-
timestamp: number;
|
|
29
|
-
rates: RateTable;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
let cachedRates: CachedRates | null = null;
|
|
33
|
-
let pendingFetch: Promise<void> | null = null;
|
|
34
|
-
|
|
35
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
36
|
-
return typeof value === "object" && value !== null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function normalizeCostCurrency(value: unknown): CostCurrencyCode | undefined {
|
|
40
|
-
if (typeof value !== "string") return undefined;
|
|
41
|
-
const code = value.trim().toUpperCase();
|
|
42
|
-
return (SUPPORTED_COST_CURRENCIES as readonly string[]).includes(code) ? code as CostCurrencyCode : undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function parseCachedRates(value: unknown): CachedRates | null {
|
|
46
|
-
if (!isRecord(value) || typeof value.timestamp !== "number" || !isRecord(value.rates)) return null;
|
|
47
|
-
|
|
48
|
-
const rates: RateTable = { USD: 1 };
|
|
49
|
-
for (const currency of SUPPORTED_COST_CURRENCIES) {
|
|
50
|
-
const rate = value.rates[currency.toLowerCase()] ?? value.rates[currency];
|
|
51
|
-
if (typeof rate === "number" && Number.isFinite(rate) && rate > 0) {
|
|
52
|
-
rates[currency] = rate;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return { timestamp: value.timestamp, rates };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function readCachedRatesFromDisk(): Promise<CachedRates | null> {
|
|
60
|
-
try {
|
|
61
|
-
return parseCachedRates(JSON.parse(await readFile(RATE_CACHE_PATH, "utf8")));
|
|
62
|
-
} catch {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
async function writeCachedRatesToDisk(rates: CachedRates): Promise<void> {
|
|
68
|
-
try {
|
|
69
|
-
await mkdir(dirname(RATE_CACHE_PATH), { recursive: true });
|
|
70
|
-
await writeFile(RATE_CACHE_PATH, JSON.stringify(rates));
|
|
71
|
-
} catch {
|
|
72
|
-
// The footer can still use in-memory rates when the cache file is unavailable.
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
async function fetchLatestRates(): Promise<CachedRates> {
|
|
77
|
-
const response = await fetch(RATE_URL);
|
|
78
|
-
if (!response.ok) throw new Error(`currency rate fetch failed with HTTP ${response.status}`);
|
|
79
|
-
|
|
80
|
-
const body = await response.json();
|
|
81
|
-
if (!isRecord(body) || !isRecord(body.usd)) throw new Error("currency rate response did not include USD rates");
|
|
82
|
-
|
|
83
|
-
const rates: RateTable = { USD: 1 };
|
|
84
|
-
for (const currency of SUPPORTED_COST_CURRENCIES) {
|
|
85
|
-
if (currency === "USD") continue;
|
|
86
|
-
const rate = body.usd[currency.toLowerCase()];
|
|
87
|
-
if (typeof rate === "number" && Number.isFinite(rate) && rate > 0) {
|
|
88
|
-
rates[currency] = rate;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return { timestamp: Date.now(), rates };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function ensureRatesRefreshing(now: number): void {
|
|
96
|
-
if (pendingFetch) return;
|
|
97
|
-
if (cachedRates && now - cachedRates.timestamp < RATE_TTL_MS) return;
|
|
98
|
-
|
|
99
|
-
pendingFetch = (async () => {
|
|
100
|
-
if (!cachedRates) cachedRates = await readCachedRatesFromDisk();
|
|
101
|
-
if (cachedRates && Date.now() - cachedRates.timestamp < RATE_TTL_MS) return;
|
|
102
|
-
if (typeof fetch !== "function") return;
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const rates = await fetchLatestRates();
|
|
106
|
-
cachedRates = rates;
|
|
107
|
-
void writeCachedRatesToDisk(rates);
|
|
108
|
-
} catch {
|
|
109
|
-
if (!cachedRates) cachedRates = await readCachedRatesFromDisk();
|
|
110
|
-
}
|
|
111
|
-
})().finally(() => {
|
|
112
|
-
pendingFetch = null;
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function getRate(currency: CostCurrencyCode): number | null {
|
|
117
|
-
if (currency === "USD") return 1;
|
|
118
|
-
|
|
119
|
-
const rate = cachedRates?.rates[currency];
|
|
120
|
-
ensureRatesRefreshing(Date.now());
|
|
121
|
-
|
|
122
|
-
return typeof rate === "number" && Number.isFinite(rate) && rate > 0 ? rate : null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export function formatUsdCost(amountUsd: number, currency: CostCurrencyCode = "USD"): string | null {
|
|
126
|
-
const rate = getRate(currency);
|
|
127
|
-
if (!rate) return `-- ${currency}`;
|
|
128
|
-
|
|
129
|
-
const amount = amountUsd * rate;
|
|
130
|
-
const decimals = currency === "JPY" || currency === "KRW" ? 0 : 2;
|
|
131
|
-
return `${CURRENCY_SYMBOLS[currency]}${amount.toFixed(decimals)}`;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function __setCurrencyRatesForTest(rates: RateTable, timestamp = Date.now()): void {
|
|
135
|
-
cachedRates = { timestamp, rates: { USD: 1, ...rates } };
|
|
136
|
-
pendingFetch = null;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
export function __resetCurrencyRatesForTest(): void {
|
|
140
|
-
cachedRates = null;
|
|
141
|
-
pendingFetch = null;
|
|
142
|
-
}
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import type { AutocompleteProvider } from "@earendil-works/pi-tui";
|
|
2
|
-
|
|
3
|
-
export function getEditorAutocompleteProvider(sourceEditor: unknown): AutocompleteProvider | undefined {
|
|
4
|
-
const candidate = sourceEditor && typeof sourceEditor === "object" ? Reflect.get(sourceEditor, "autocompleteProvider") : null;
|
|
5
|
-
if (!candidate || typeof candidate !== "object") {
|
|
6
|
-
return undefined;
|
|
7
|
-
}
|
|
8
|
-
if (typeof Reflect.get(candidate, "getSuggestions") !== "function") {
|
|
9
|
-
return undefined;
|
|
10
|
-
}
|
|
11
|
-
if (typeof Reflect.get(candidate, "applyCompletion") !== "function") {
|
|
12
|
-
return undefined;
|
|
13
|
-
}
|
|
14
|
-
return candidate;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function passAutocompleteProviderThroughPreviousEditor(
|
|
18
|
-
provider: AutocompleteProvider,
|
|
19
|
-
previousEditor: unknown,
|
|
20
|
-
): AutocompleteProvider {
|
|
21
|
-
if (!previousEditor || typeof previousEditor !== "object") {
|
|
22
|
-
return provider;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const setAutocompleteProvider = Reflect.get(previousEditor, "setAutocompleteProvider");
|
|
26
|
-
if (typeof setAutocompleteProvider !== "function") {
|
|
27
|
-
return getEditorAutocompleteProvider(previousEditor) ?? provider;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
setAutocompleteProvider.call(previousEditor, provider);
|
|
31
|
-
return getEditorAutocompleteProvider(previousEditor) ?? provider;
|
|
32
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
export interface EditorPerfOptions {
|
|
2
|
-
enabled: boolean;
|
|
3
|
-
fastRender: boolean;
|
|
4
|
-
editorChrome: boolean;
|
|
5
|
-
widgets: boolean;
|
|
6
|
-
bashWidgets: boolean;
|
|
7
|
-
lastPrompt: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface EditorPerfMetric {
|
|
11
|
-
count: number;
|
|
12
|
-
totalMs: number;
|
|
13
|
-
maxMs: number;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function readEditorPerfOptions(
|
|
17
|
-
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
18
|
-
): EditorPerfOptions {
|
|
19
|
-
const enabled = env.POWERLINE_DEBUG_PERF === "1";
|
|
20
|
-
return {
|
|
21
|
-
enabled,
|
|
22
|
-
fastRender: !enabled || env.POWERLINE_PERF_FAST_RENDER !== "0",
|
|
23
|
-
editorChrome: !enabled || env.POWERLINE_PERF_EDITOR_CHROME !== "0",
|
|
24
|
-
widgets: !enabled || env.POWERLINE_PERF_WIDGETS !== "0",
|
|
25
|
-
bashWidgets: !enabled || env.POWERLINE_PERF_BASH_WIDGETS !== "0",
|
|
26
|
-
lastPrompt: !enabled || env.POWERLINE_PERF_LAST_PROMPT !== "0",
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export class EditorPerfProfiler {
|
|
31
|
-
readonly options: EditorPerfOptions;
|
|
32
|
-
private readonly metrics = new Map<string, EditorPerfMetric>();
|
|
33
|
-
private startedAt = Date.now();
|
|
34
|
-
private maxDraftChars = 0;
|
|
35
|
-
private draftObservationCount = 0;
|
|
36
|
-
|
|
37
|
-
constructor(options: EditorPerfOptions) {
|
|
38
|
-
this.options = options;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
measure<T>(name: string, operation: () => T): T {
|
|
42
|
-
const startedAt = performance.now();
|
|
43
|
-
try {
|
|
44
|
-
return operation();
|
|
45
|
-
} finally {
|
|
46
|
-
this.record(name, performance.now() - startedAt);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
count(name: string): void {
|
|
51
|
-
const metric = this.metrics.get(name) ?? { count: 0, totalMs: 0, maxMs: 0 };
|
|
52
|
-
metric.count += 1;
|
|
53
|
-
this.metrics.set(name, metric);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
observeDraft(lines: readonly unknown[]): void {
|
|
57
|
-
this.draftObservationCount += 1;
|
|
58
|
-
if (lines.length > 1 && this.draftObservationCount % 32 !== 1) return;
|
|
59
|
-
|
|
60
|
-
let chars = Math.max(0, lines.length - 1);
|
|
61
|
-
for (const line of lines) {
|
|
62
|
-
if (typeof line === "string") chars += line.length;
|
|
63
|
-
}
|
|
64
|
-
this.maxDraftChars = Math.max(this.maxDraftChars, chars);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
reset(): void {
|
|
68
|
-
this.metrics.clear();
|
|
69
|
-
this.startedAt = Date.now();
|
|
70
|
-
this.maxDraftChars = 0;
|
|
71
|
-
this.draftObservationCount = 0;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
report(): string {
|
|
75
|
-
const inputCount = this.metrics.get("input.total")?.count ?? 0;
|
|
76
|
-
const renderCount = this.metrics.get("editor.render.total")?.count ?? 0;
|
|
77
|
-
const lines = [
|
|
78
|
-
`Powerline editor perf (${((Date.now() - this.startedAt) / 1000).toFixed(1)}s)`,
|
|
79
|
-
`A/B: fast-render=${this.options.fastRender ? "on" : "off"}, editor-chrome=${this.options.editorChrome ? "on" : "off"}, widgets=${this.options.widgets ? "on" : "off"}, bash-widgets=${this.options.bashWidgets ? "on" : "off"}, last-prompt=${this.options.lastPrompt ? "on" : "off"}`,
|
|
80
|
-
`inputs=${inputCount}, editor-renders=${renderCount}, renders/input=${inputCount > 0 ? (renderCount / inputCount).toFixed(2) : "0.00"}, max-draft=${this.maxDraftChars}`,
|
|
81
|
-
];
|
|
82
|
-
|
|
83
|
-
for (const [name, metric] of [...this.metrics].sort(([left], [right]) => left.localeCompare(right))) {
|
|
84
|
-
lines.push(metric.totalMs === 0 && metric.maxMs === 0
|
|
85
|
-
? `${name}: n=${metric.count}`
|
|
86
|
-
: `${name}: n=${metric.count}, avg=${(metric.totalMs / metric.count).toFixed(3)}ms, max=${metric.maxMs.toFixed(3)}ms, total=${metric.totalMs.toFixed(1)}ms`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return lines.join("\n");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private record(name: string, durationMs: number): void {
|
|
93
|
-
const metric = this.metrics.get(name) ?? { count: 0, totalMs: 0, maxMs: 0 };
|
|
94
|
-
metric.count += 1;
|
|
95
|
-
metric.totalMs += durationMs;
|
|
96
|
-
metric.maxMs = Math.max(metric.maxMs, durationMs);
|
|
97
|
-
this.metrics.set(name, metric);
|
|
98
|
-
}
|
|
99
|
-
}
|