pi-zentui 0.1.10 → 0.1.12
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/README.md +120 -119
- package/extensions/zentui/config.ts +103 -3
- package/extensions/zentui/extension-status.ts +53 -0
- package/extensions/zentui/footer.ts +137 -9
- package/extensions/zentui/index.ts +17 -10
- package/extensions/zentui/settings-command.ts +115 -12
- package/extensions/zentui/style.ts +34 -3
- package/extensions/zentui/ui.ts +73 -11
- package/extensions/zentui/user-message.ts +30 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,12 +22,14 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
22
22
|
- `[!?↑]` — git status indicators (modified, untracked, ahead/behind, stashed, etc.)
|
|
23
23
|
- `via v5.5.0` — runtime detection with version and Starship-style Nerd Font runtime/language modules
|
|
24
24
|
- Right side shows context usage, token counts, and cost
|
|
25
|
+
- Third-party Pi extension statuses from `ctx.ui.setStatus()` can be shown on the left,
|
|
26
|
+
middle, or right side, or hidden per status key from `/zentui`
|
|
25
27
|
|
|
26
28
|
### Editor (Opencode-inspired)
|
|
27
29
|
|
|
28
|
-
- Bordered input box with
|
|
30
|
+
- Bordered input box with configurable accent rail and border colors
|
|
29
31
|
- Model name and provider displayed inside the editor frame
|
|
30
|
-
-
|
|
32
|
+
- Configurable model, provider, and thinking-level indicator colors
|
|
31
33
|
- Prompt-box-style user messages matching the ZentUI input chrome
|
|
32
34
|
|
|
33
35
|
### Git Status Icons
|
|
@@ -49,65 +51,65 @@ Zentui brings two popular aesthetics to Pi:
|
|
|
49
51
|
|
|
50
52
|
Detects Starship Nerd Font runtime/language modules, uses the Starship Nerd Font symbols, and keeps Starship-style defaults such as `bold green` for Node.js. By default Zentui maps those styles through your active Pi theme; switch the Starship/footer color source to `terminal` in `/zentui` if you want your terminal colorscheme to supply the exact ANSI colors.
|
|
51
53
|
|
|
52
|
-
| Runtime/language | Detection examples
|
|
53
|
-
| ---------------- |
|
|
54
|
-
| Buf | `buf.yaml`, `buf.gen.yaml`, `buf.work.yaml`
|
|
55
|
-
| Bun | `bun.lock`, `bun.lockb`
|
|
56
|
-
| C | `.c`, `.h` files
|
|
57
|
-
| C++ | `.cpp`, `.cc`, `.cxx`, `.hpp` files
|
|
58
|
-
| CMake | `CMakeLists.txt`, `CMakeCache.txt`
|
|
59
|
-
| COBOL | `.cbl`, `.cob` files
|
|
60
|
-
| Conda | `CONDA_DEFAULT_ENV` environment
|
|
61
|
-
| Crystal | `.cr` files, `shard.yml`
|
|
62
|
-
| Dart | `.dart` files, `pubspec.yaml`, `.dart_tool/`
|
|
63
|
-
| Deno | `deno.json`, `deno.jsonc`, `deno.lock`
|
|
64
|
-
| .NET | `.csproj`, `.fsproj`, `global.json`, `Directory.Build.*`
|
|
65
|
-
| Elixir | `mix.exs`
|
|
66
|
-
| Elm | `.elm` files, `elm.json`, `elm-stuff/`
|
|
67
|
-
| Erlang | `rebar.config`, `erlang.mk`
|
|
68
|
-
| Fennel | `.fnl` files
|
|
69
|
-
| Fortran | `.f`, `.f90`, `.f95`, `.f03`, `.f08`, `.f18`, `fpm.toml`
|
|
70
|
-
| Gleam | `.gleam` files, `gleam.toml`
|
|
71
|
-
| Go | `go.mod`
|
|
72
|
-
| Gradle | `build.gradle`, `build.gradle.kts`, `gradle/`
|
|
73
|
-
| Guix shell | `GUIX_ENVIRONMENT` environment
|
|
74
|
-
| Haskell | `.hs`, `.cabal`, `stack.yaml`, `cabal.project`
|
|
75
|
-
| Haxe | `.hx`, `.hxml`, `haxelib.json`, `.haxerc`
|
|
76
|
-
| Helm | `helmfile.yaml`, `Chart.yaml`
|
|
77
|
-
| Java | `.java-version`
|
|
78
|
-
| Julia | `.jl` files, `Project.toml`, `Manifest.toml`
|
|
79
|
-
| Kotlin | `.kt`, `.kts` files
|
|
80
|
-
| Lua | `.lua` files, `stylua.toml`, `.luarc.json`, `lua/` dir
|
|
81
|
-
| Maven | `pom.xml`
|
|
82
|
-
| Meson | `MESON_DEVENV=1` and `MESON_PROJECT_NAME` environment
|
|
83
|
-
| Mojo | `.mojo` files
|
|
84
|
-
| Nim | `.nim`, `.nims`, `.nimble`, `nim.cfg`
|
|
85
|
-
| Nix shell | `IN_NIX_SHELL=pure` or `IN_NIX_SHELL=impure` environment
|
|
86
|
-
| Node.js | `package.json`, `.nvmrc`, `.node-version`
|
|
87
|
-
| OCaml | `.opam`, `.ml`, `.mli`, `dune`, `_opam/`, `esy.lock/`
|
|
88
|
-
| Odin | `.odin` files
|
|
89
|
-
| OPA/Rego | `.rego` files
|
|
90
|
-
| Perl | `.pl`, `.pm`, `Makefile.PL`, `cpanfile`, `META.*`
|
|
91
|
-
| PHP | `composer.json`
|
|
54
|
+
| Runtime/language | Detection examples |
|
|
55
|
+
| ---------------- | ------------------------------------------------------------- |
|
|
56
|
+
| Buf | `buf.yaml`, `buf.gen.yaml`, `buf.work.yaml` |
|
|
57
|
+
| Bun | `bun.lock`, `bun.lockb` |
|
|
58
|
+
| C | `.c`, `.h` files |
|
|
59
|
+
| C++ | `.cpp`, `.cc`, `.cxx`, `.hpp` files |
|
|
60
|
+
| CMake | `CMakeLists.txt`, `CMakeCache.txt` |
|
|
61
|
+
| COBOL | `.cbl`, `.cob` files |
|
|
62
|
+
| Conda | `CONDA_DEFAULT_ENV` environment |
|
|
63
|
+
| Crystal | `.cr` files, `shard.yml` |
|
|
64
|
+
| Dart | `.dart` files, `pubspec.yaml`, `.dart_tool/` |
|
|
65
|
+
| Deno | `deno.json`, `deno.jsonc`, `deno.lock` |
|
|
66
|
+
| .NET | `.csproj`, `.fsproj`, `global.json`, `Directory.Build.*` |
|
|
67
|
+
| Elixir | `mix.exs` |
|
|
68
|
+
| Elm | `.elm` files, `elm.json`, `elm-stuff/` |
|
|
69
|
+
| Erlang | `rebar.config`, `erlang.mk` |
|
|
70
|
+
| Fennel | `.fnl` files |
|
|
71
|
+
| Fortran | `.f`, `.f90`, `.f95`, `.f03`, `.f08`, `.f18`, `fpm.toml` |
|
|
72
|
+
| Gleam | `.gleam` files, `gleam.toml` |
|
|
73
|
+
| Go | `go.mod` |
|
|
74
|
+
| Gradle | `build.gradle`, `build.gradle.kts`, `gradle/` |
|
|
75
|
+
| Guix shell | `GUIX_ENVIRONMENT` environment |
|
|
76
|
+
| Haskell | `.hs`, `.cabal`, `stack.yaml`, `cabal.project` |
|
|
77
|
+
| Haxe | `.hx`, `.hxml`, `haxelib.json`, `.haxerc` |
|
|
78
|
+
| Helm | `helmfile.yaml`, `Chart.yaml` |
|
|
79
|
+
| Java | `.java-version` |
|
|
80
|
+
| Julia | `.jl` files, `Project.toml`, `Manifest.toml` |
|
|
81
|
+
| Kotlin | `.kt`, `.kts` files |
|
|
82
|
+
| Lua | `.lua` files, `stylua.toml`, `.luarc.json`, `lua/` dir |
|
|
83
|
+
| Maven | `pom.xml` |
|
|
84
|
+
| Meson | `MESON_DEVENV=1` and `MESON_PROJECT_NAME` environment |
|
|
85
|
+
| Mojo | `.mojo` files |
|
|
86
|
+
| Nim | `.nim`, `.nims`, `.nimble`, `nim.cfg` |
|
|
87
|
+
| Nix shell | `IN_NIX_SHELL=pure` or `IN_NIX_SHELL=impure` environment |
|
|
88
|
+
| Node.js | `package.json`, `.nvmrc`, `.node-version` |
|
|
89
|
+
| OCaml | `.opam`, `.ml`, `.mli`, `dune`, `_opam/`, `esy.lock/` |
|
|
90
|
+
| Odin | `.odin` files |
|
|
91
|
+
| OPA/Rego | `.rego` files |
|
|
92
|
+
| Perl | `.pl`, `.pm`, `Makefile.PL`, `cpanfile`, `META.*` |
|
|
93
|
+
| PHP | `composer.json` |
|
|
92
94
|
| Pixi | `pixi.toml`, `pixi.lock`, `PIXI_ENVIRONMENT_NAME` environment |
|
|
93
|
-
| Pulumi | `Pulumi.yaml`, `Pulumi.yml`
|
|
94
|
-
| PureScript | `.purs` files, `spago.dhall`, `spago.yaml`, `spago.lock`
|
|
95
|
-
| Python | `pyproject.toml`, `requirements.txt`, `setup.py`, `Pipfile`
|
|
96
|
-
| R | `.R`, `.Rmd`, `.Rproj`, `DESCRIPTION`, `.Rproj.user/`
|
|
97
|
-
| Raku | `.raku`, `.rakumod`, `.p6`, `.pm6`, `META6.json`
|
|
98
|
-
| Red | `.red`, `.reds` files
|
|
99
|
-
| Ruby | `Gemfile`, `.ruby-version`
|
|
95
|
+
| Pulumi | `Pulumi.yaml`, `Pulumi.yml` |
|
|
96
|
+
| PureScript | `.purs` files, `spago.dhall`, `spago.yaml`, `spago.lock` |
|
|
97
|
+
| Python | `pyproject.toml`, `requirements.txt`, `setup.py`, `Pipfile` |
|
|
98
|
+
| R | `.R`, `.Rmd`, `.Rproj`, `DESCRIPTION`, `.Rproj.user/` |
|
|
99
|
+
| Raku | `.raku`, `.rakumod`, `.p6`, `.pm6`, `META6.json` |
|
|
100
|
+
| Red | `.red`, `.reds` files |
|
|
101
|
+
| Ruby | `Gemfile`, `.ruby-version` |
|
|
100
102
|
| Rust | `Cargo.toml` |
|
|
101
|
-
| Scala | `.scala`, `.sbt`, `build.sbt`, `.metals/`
|
|
102
|
-
| Solidity | `.sol` files
|
|
103
|
-
| Spack | `SPACK_ENV` environment
|
|
104
|
-
| Swift | `.swift` files, `Package.swift`
|
|
105
|
-
| Terraform | `.tf`, `.tfplan`, `.tfstate`, `.terraform/`
|
|
106
|
-
| Typst | `.typ` files, `template.typ`
|
|
107
|
-
| Vagrant | `Vagrantfile`
|
|
108
|
-
| V | `.v` files, `v.mod`, `vpkg.json`
|
|
109
|
-
| Xmake | `xmake.lua`
|
|
110
|
-
| Zig | `.zig` files, `build.zig`
|
|
103
|
+
| Scala | `.scala`, `.sbt`, `build.sbt`, `.metals/` |
|
|
104
|
+
| Solidity | `.sol` files |
|
|
105
|
+
| Spack | `SPACK_ENV` environment |
|
|
106
|
+
| Swift | `.swift` files, `Package.swift` |
|
|
107
|
+
| Terraform | `.tf`, `.tfplan`, `.tfstate`, `.terraform/` |
|
|
108
|
+
| Typst | `.typ` files, `template.typ` |
|
|
109
|
+
| Vagrant | `Vagrantfile` |
|
|
110
|
+
| V | `.v` files, `v.mod`, `vpkg.json` |
|
|
111
|
+
| Xmake | `xmake.lua` |
|
|
112
|
+
| Zig | `.zig` files, `build.zig` |
|
|
111
113
|
|
|
112
114
|
## Install
|
|
113
115
|
|
|
@@ -121,73 +123,72 @@ pi install git:github.com/lmilojevicc/pi-zentui
|
|
|
121
123
|
|
|
122
124
|
## Config
|
|
123
125
|
|
|
124
|
-
|
|
126
|
+
User config lives at `~/.pi/agent/zentui.json`. The file is optional: missing or invalid known values fall back to Zentui defaults, unknown keys are ignored at runtime, and `/zentui` can patch color-source settings plus active third-party status placements.
|
|
125
127
|
|
|
126
|
-
|
|
127
|
-
~/.pi/agent/zentui.json
|
|
128
|
-
```
|
|
129
|
-
|
|
130
|
-
Zentui treats this file as user-owned and compatibility-sensitive: invalid known values fall back to runtime defaults, unknown keys are ignored at runtime, and `/zentui` patches only the settings it changes instead of rewriting the whole file.
|
|
131
|
-
|
|
132
|
-
### Runtime defaults
|
|
128
|
+
Default config values — copy this and change any value you want:
|
|
133
129
|
|
|
134
130
|
```json
|
|
135
131
|
{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
132
|
+
"projectRefreshIntervalMs": 30000,
|
|
133
|
+
"icons": {
|
|
134
|
+
"cwd": "",
|
|
135
|
+
"git": "",
|
|
136
|
+
"ahead": "↑",
|
|
137
|
+
"behind": "↓",
|
|
138
|
+
"diverged": "⇕",
|
|
139
|
+
"conflicted": "=",
|
|
140
|
+
"untracked": "?",
|
|
141
|
+
"stashed": "$",
|
|
142
|
+
"modified": "!",
|
|
143
|
+
"staged": "+",
|
|
144
|
+
"renamed": "»",
|
|
145
|
+
"deleted": "✘",
|
|
146
|
+
"typechanged": "T"
|
|
147
|
+
},
|
|
148
|
+
"colors": {
|
|
149
|
+
"cwd": "bold cyan",
|
|
150
|
+
"gitBranch": "bold purple",
|
|
151
|
+
"gitStatus": "bold red",
|
|
152
|
+
"contextNormal": "bright-black",
|
|
153
|
+
"contextWarning": "bold yellow",
|
|
154
|
+
"contextError": "bold red",
|
|
155
|
+
"tokens": "bright-black",
|
|
156
|
+
"cost": "bold green",
|
|
157
|
+
"extensionStatus": "bright-black",
|
|
158
|
+
"separator": "bright-black",
|
|
159
|
+
"runtimePrefix": "",
|
|
160
|
+
"editorAccent": "accent",
|
|
161
|
+
"editorBorder": "borderMuted",
|
|
162
|
+
"editorModel": "accent",
|
|
163
|
+
"editorProvider": "text",
|
|
164
|
+
"editorThinking": "muted",
|
|
165
|
+
"editorThinkingMinimal": "thinkingMinimal",
|
|
166
|
+
"editorThinkingLow": "thinkingLow",
|
|
167
|
+
"editorThinkingMedium": "thinkingMedium",
|
|
168
|
+
"editorThinkingHigh": "thinkingHigh",
|
|
169
|
+
"editorThinkingXhigh": "thinkingXhigh"
|
|
170
|
+
},
|
|
171
|
+
"colorSources": {
|
|
172
|
+
"starship": "theme",
|
|
173
|
+
"editor": "theme",
|
|
174
|
+
"userMessages": "theme"
|
|
175
|
+
},
|
|
176
|
+
"extensionStatuses": {
|
|
177
|
+
"defaultPlacement": "right",
|
|
178
|
+
"placements": {}
|
|
179
|
+
}
|
|
169
180
|
}
|
|
170
181
|
```
|
|
171
182
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
- `
|
|
179
|
-
- `
|
|
180
|
-
|
|
181
|
-
Both settings default to `theme`. The config still stores editor and previous user-message sources separately as `editor` and `userMessages`, but `/zentui` changes them together so the prompt chrome stays consistent.
|
|
182
|
-
|
|
183
|
-
Color values can use terminal-palette style strings, hex colors, or Pi theme color tokens:
|
|
184
|
-
|
|
185
|
-
- Named terminal colors: `bold purple`, `yellow`, `bright-cyan`
|
|
186
|
-
- 256-color / hex: `bold 149`, `fg:202`, `#89b4fa`
|
|
187
|
-
- Backgrounds and modifiers: `bg:blue fg:bright-green`, `underline bg:#bf5700`
|
|
188
|
-
- Pi theme tokens: `accent`, `borderMuted`, `syntaxKeyword`
|
|
189
|
-
|
|
190
|
-
Use the `colors` config key for these values.
|
|
183
|
+
- Style values can be Starship/terminal strings (`bold purple`, `fg:202`, `#89b4fa`, `bg:blue fg:bright-green`) or Pi theme tokens (`accent`, `borderMuted`, `thinkingHigh`).
|
|
184
|
+
- `projectRefreshIntervalMs`: project status polling interval; `0` disables polling.
|
|
185
|
+
- `icons`: every shown icon key is configurable; omit any key to use the Zentui default.
|
|
186
|
+
- `colorSources`: `theme` maps styles through Pi theme tokens; `terminal` emits terminal colors. `/zentui` switches these sources; manual JSON controls specific style values.
|
|
187
|
+
- `extensionStatuses`: controls third-party statuses published by other Pi extensions through `ctx.ui.setStatus()`. `defaultPlacement` and each `placements` value can be `off`, `left`, `middle`, or `right`. `/zentui` lists only statuses that are currently active.
|
|
188
|
+
- The shown `editor*` values match the default `theme` source. Omit those keys to keep Zentui's source-aware defaults when switching between `theme` and `terminal`.
|
|
189
|
+
- `editorAccent` styles the active editor rail and previous user-message rail.
|
|
190
|
+
- `editorBorder` styles the active editor and previous user-message top/bottom border color only; the border glyph stays `─`.
|
|
191
|
+
- `editorModel`, `editorProvider`, and `editorThinking*` style the editor metadata. `editorThinking` applies to every non-`off` thinking level unless a level-specific key is set.
|
|
191
192
|
|
|
192
193
|
## Requirements
|
|
193
194
|
|
|
@@ -12,6 +12,13 @@ export type ColorSourcesConfig = {
|
|
|
12
12
|
userMessages: ColorSource;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
export type ExtensionStatusPlacement = "off" | "left" | "middle" | "right";
|
|
16
|
+
|
|
17
|
+
export type ExtensionStatusesConfig = {
|
|
18
|
+
defaultPlacement: ExtensionStatusPlacement;
|
|
19
|
+
placements: Record<string, ExtensionStatusPlacement>;
|
|
20
|
+
};
|
|
21
|
+
|
|
15
22
|
const DEFAULT_PROJECT_REFRESH_INTERVAL_MS = 30_000;
|
|
16
23
|
const MIN_PROJECT_REFRESH_INTERVAL_MS = 5_000;
|
|
17
24
|
|
|
@@ -43,8 +50,20 @@ export type PolishedTuiConfig = {
|
|
|
43
50
|
cost: ColorSpec;
|
|
44
51
|
separator: ColorSpec;
|
|
45
52
|
runtimePrefix: ColorSpec;
|
|
53
|
+
extensionStatus: ColorSpec;
|
|
54
|
+
editorAccent?: ColorSpec;
|
|
55
|
+
editorBorder?: ColorSpec;
|
|
56
|
+
editorModel?: ColorSpec;
|
|
57
|
+
editorProvider?: ColorSpec;
|
|
58
|
+
editorThinking?: ColorSpec;
|
|
59
|
+
editorThinkingMinimal?: ColorSpec;
|
|
60
|
+
editorThinkingLow?: ColorSpec;
|
|
61
|
+
editorThinkingMedium?: ColorSpec;
|
|
62
|
+
editorThinkingHigh?: ColorSpec;
|
|
63
|
+
editorThinkingXhigh?: ColorSpec;
|
|
46
64
|
};
|
|
47
65
|
colorSources: ColorSourcesConfig;
|
|
66
|
+
extensionStatuses: ExtensionStatusesConfig;
|
|
48
67
|
};
|
|
49
68
|
|
|
50
69
|
export const configPath = join(getAgentDir(), "zentui.json");
|
|
@@ -70,19 +89,24 @@ export const defaultConfig: PolishedTuiConfig = {
|
|
|
70
89
|
cwd: "bold cyan",
|
|
71
90
|
gitBranch: "bold purple",
|
|
72
91
|
gitStatus: "bold red",
|
|
73
|
-
contextNormal: "
|
|
92
|
+
contextNormal: "bright-black",
|
|
74
93
|
contextWarning: "bold yellow",
|
|
75
94
|
contextError: "bold red",
|
|
76
|
-
tokens: "
|
|
95
|
+
tokens: "bright-black",
|
|
77
96
|
cost: "bold green",
|
|
78
|
-
separator: "
|
|
97
|
+
separator: "bright-black",
|
|
79
98
|
runtimePrefix: "",
|
|
99
|
+
extensionStatus: "bright-black",
|
|
80
100
|
},
|
|
81
101
|
colorSources: {
|
|
82
102
|
starship: "theme",
|
|
83
103
|
editor: "theme",
|
|
84
104
|
userMessages: "theme",
|
|
85
105
|
},
|
|
106
|
+
extensionStatuses: {
|
|
107
|
+
defaultPlacement: "right",
|
|
108
|
+
placements: {},
|
|
109
|
+
},
|
|
86
110
|
};
|
|
87
111
|
|
|
88
112
|
const iconKeys = [
|
|
@@ -168,6 +192,17 @@ function normalizeColors(record: Record<string, unknown>): Partial<PolishedTuiCo
|
|
|
168
192
|
cost: colorValue(record, "cost"),
|
|
169
193
|
separator: colorValue(record, "separator"),
|
|
170
194
|
runtimePrefix: colorValue(record, "runtimePrefix"),
|
|
195
|
+
extensionStatus: colorValue(record, "extensionStatus"),
|
|
196
|
+
editorAccent: colorValue(record, "editorAccent"),
|
|
197
|
+
editorBorder: colorValue(record, "editorBorder"),
|
|
198
|
+
editorModel: colorValue(record, "editorModel"),
|
|
199
|
+
editorProvider: colorValue(record, "editorProvider"),
|
|
200
|
+
editorThinking: colorValue(record, "editorThinking"),
|
|
201
|
+
editorThinkingMinimal: colorValue(record, "editorThinkingMinimal"),
|
|
202
|
+
editorThinkingLow: colorValue(record, "editorThinkingLow"),
|
|
203
|
+
editorThinkingMedium: colorValue(record, "editorThinkingMedium"),
|
|
204
|
+
editorThinkingHigh: colorValue(record, "editorThinkingHigh"),
|
|
205
|
+
editorThinkingXhigh: colorValue(record, "editorThinkingXhigh"),
|
|
171
206
|
});
|
|
172
207
|
}
|
|
173
208
|
|
|
@@ -179,6 +214,29 @@ function normalizeColorSources(record: Record<string, unknown>): ColorSourcesCon
|
|
|
179
214
|
};
|
|
180
215
|
}
|
|
181
216
|
|
|
217
|
+
export function isExtensionStatusPlacement(value: unknown): value is ExtensionStatusPlacement {
|
|
218
|
+
return value === "off" || value === "left" || value === "middle" || value === "right";
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizeExtensionStatuses(record: Record<string, unknown>): ExtensionStatusesConfig {
|
|
222
|
+
const defaultPlacement = isExtensionStatusPlacement(record.defaultPlacement)
|
|
223
|
+
? record.defaultPlacement
|
|
224
|
+
: defaultConfig.extensionStatuses.defaultPlacement;
|
|
225
|
+
const placements = isRecord(record.placements)
|
|
226
|
+
? Object.fromEntries(
|
|
227
|
+
Object.entries(record.placements).filter(
|
|
228
|
+
(entry): entry is [string, ExtensionStatusPlacement] =>
|
|
229
|
+
isExtensionStatusPlacement(entry[1]),
|
|
230
|
+
),
|
|
231
|
+
)
|
|
232
|
+
: {};
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
defaultPlacement,
|
|
236
|
+
placements,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
182
240
|
function isColorSourceKey(value: string): value is keyof ColorSourcesConfig {
|
|
183
241
|
return value === "starship" || value === "editor" || value === "userMessages";
|
|
184
242
|
}
|
|
@@ -220,6 +278,9 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
220
278
|
const colorSources = isRecord(config.colorSources)
|
|
221
279
|
? normalizeColorSources(config.colorSources as Record<string, unknown>)
|
|
222
280
|
: defaultConfig.colorSources;
|
|
281
|
+
const extensionStatuses = isRecord(config.extensionStatuses)
|
|
282
|
+
? normalizeExtensionStatuses(config.extensionStatuses as Record<string, unknown>)
|
|
283
|
+
: defaultConfig.extensionStatuses;
|
|
223
284
|
return {
|
|
224
285
|
projectRefreshIntervalMs: parseProjectRefreshIntervalMs(config.projectRefreshIntervalMs),
|
|
225
286
|
icons: {
|
|
@@ -231,9 +292,20 @@ export function mergeConfig(parsed: unknown): PolishedTuiConfig {
|
|
|
231
292
|
...colors,
|
|
232
293
|
},
|
|
233
294
|
colorSources: { ...colorSources },
|
|
295
|
+
extensionStatuses: {
|
|
296
|
+
defaultPlacement: extensionStatuses.defaultPlacement,
|
|
297
|
+
placements: { ...extensionStatuses.placements },
|
|
298
|
+
},
|
|
234
299
|
};
|
|
235
300
|
}
|
|
236
301
|
|
|
302
|
+
export function getExtensionStatusPlacement(
|
|
303
|
+
config: PolishedTuiConfig,
|
|
304
|
+
key: string,
|
|
305
|
+
): ExtensionStatusPlacement {
|
|
306
|
+
return config.extensionStatuses.placements[key] ?? config.extensionStatuses.defaultPlacement;
|
|
307
|
+
}
|
|
308
|
+
|
|
237
309
|
export function loadConfig(): PolishedTuiConfig {
|
|
238
310
|
try {
|
|
239
311
|
if (!existsSync(configPath)) return mergeConfig({});
|
|
@@ -258,3 +330,31 @@ export function saveColorSourcesPatch(
|
|
|
258
330
|
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
259
331
|
return mergeConfig(record);
|
|
260
332
|
}
|
|
333
|
+
|
|
334
|
+
export function saveExtensionStatusPlacement(
|
|
335
|
+
key: string,
|
|
336
|
+
placement: ExtensionStatusPlacement,
|
|
337
|
+
path = configPath,
|
|
338
|
+
): PolishedTuiConfig {
|
|
339
|
+
const record = readConfigRecord(path);
|
|
340
|
+
const existingExtensionStatuses = isRecord(record.extensionStatuses)
|
|
341
|
+
? { ...(record.extensionStatuses as Record<string, unknown>) }
|
|
342
|
+
: {};
|
|
343
|
+
const existingPlacements = isRecord(existingExtensionStatuses.placements)
|
|
344
|
+
? { ...(existingExtensionStatuses.placements as Record<string, unknown>) }
|
|
345
|
+
: {};
|
|
346
|
+
|
|
347
|
+
Object.defineProperty(existingPlacements, key, {
|
|
348
|
+
value: placement,
|
|
349
|
+
enumerable: true,
|
|
350
|
+
configurable: true,
|
|
351
|
+
writable: true,
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
record.extensionStatuses = {
|
|
355
|
+
...existingExtensionStatuses,
|
|
356
|
+
placements: existingPlacements,
|
|
357
|
+
};
|
|
358
|
+
writeFileSync(path, `${JSON.stringify(record, null, 2)}\n`, "utf8");
|
|
359
|
+
return mergeConfig(record);
|
|
360
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import type { ExtensionStatusPlacement, PolishedTuiConfig } from "./config";
|
|
3
|
+
import { getExtensionStatusPlacement } from "./config";
|
|
4
|
+
|
|
5
|
+
export type ExtensionStatusSegment = {
|
|
6
|
+
key: string;
|
|
7
|
+
text: string;
|
|
8
|
+
placement: ExtensionStatusPlacement;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ExtensionStatusSegmentsByPlacement = {
|
|
12
|
+
left: ExtensionStatusSegment[];
|
|
13
|
+
middle: ExtensionStatusSegment[];
|
|
14
|
+
right: ExtensionStatusSegment[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function compareKeys(a: ExtensionStatusSegment, b: ExtensionStatusSegment): number {
|
|
18
|
+
return a.key < b.key ? -1 : a.key > b.key ? 1 : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function sanitizeExtensionStatusText(value: string): string {
|
|
22
|
+
return stripVTControlCharacters(value)
|
|
23
|
+
.replace(/[\r\n\t\f\v]+/g, " ")
|
|
24
|
+
.replace(/[\u0000-\u001f\u007f-\u009f]/g, "")
|
|
25
|
+
.replace(/\s+/g, " ")
|
|
26
|
+
.trim();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function collectExtensionStatusSegments(
|
|
30
|
+
statuses: ReadonlyMap<string, string>,
|
|
31
|
+
config: PolishedTuiConfig,
|
|
32
|
+
): ExtensionStatusSegmentsByPlacement {
|
|
33
|
+
const segments: ExtensionStatusSegmentsByPlacement = {
|
|
34
|
+
left: [],
|
|
35
|
+
middle: [],
|
|
36
|
+
right: [],
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
for (const [key, value] of statuses.entries()) {
|
|
40
|
+
const placement = getExtensionStatusPlacement(config, key);
|
|
41
|
+
if (placement === "off") continue;
|
|
42
|
+
|
|
43
|
+
const text = sanitizeExtensionStatusText(value);
|
|
44
|
+
if (!text) continue;
|
|
45
|
+
|
|
46
|
+
segments[placement].push({ key, text, placement });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
segments.left.sort(compareKeys);
|
|
50
|
+
segments.middle.sort(compareKeys);
|
|
51
|
+
segments.right.sort(compareKeys);
|
|
52
|
+
return segments;
|
|
53
|
+
}
|
|
@@ -1,10 +1,129 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import type { PolishedTuiConfig } from "./config";
|
|
4
|
+
import { collectExtensionStatusSegments } from "./extension-status";
|
|
4
5
|
import { formatCwdLabel, formatRuntimeSegment } from "./format";
|
|
5
6
|
import type { FooterState } from "./state";
|
|
6
7
|
import { renderStyleForSource } from "./style";
|
|
7
8
|
|
|
9
|
+
function joinStatusTexts(statusTexts: string[], separator: string): string {
|
|
10
|
+
return statusTexts.filter(Boolean).join(separator);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function fitStatusTexts(statusTexts: string[], maxWidth: number, separator: string): string {
|
|
14
|
+
if (maxWidth <= 0) return "";
|
|
15
|
+
|
|
16
|
+
const fitted: string[] = [];
|
|
17
|
+
for (const text of statusTexts) {
|
|
18
|
+
const candidate = joinStatusTexts([...fitted, text], separator);
|
|
19
|
+
if (visibleWidth(candidate) <= maxWidth) {
|
|
20
|
+
fitted.push(text);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (fitted.length === 0) {
|
|
25
|
+
return maxWidth > 1 ? truncateToWidth(text, maxWidth, "…") : "";
|
|
26
|
+
}
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return joinStatusTexts(fitted, separator);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function appendStatusArea(base: string, statusText: string, separator: string): string {
|
|
34
|
+
if (!base) return statusText;
|
|
35
|
+
if (!statusText) return base;
|
|
36
|
+
return `${base}${separator}${statusText}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function prependStatusArea(base: string, statusText: string, separator: string): string {
|
|
40
|
+
if (!base) return statusText;
|
|
41
|
+
if (!statusText) return base;
|
|
42
|
+
return `${statusText}${separator}${base}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function composeBuiltInFooterContent(left: string, right: string, innerWidth: number): string {
|
|
46
|
+
const leftWidth = visibleWidth(left);
|
|
47
|
+
const rightWidth = visibleWidth(right);
|
|
48
|
+
return leftWidth >= innerWidth
|
|
49
|
+
? truncateToWidth(left, innerWidth, "")
|
|
50
|
+
: leftWidth + 1 + rightWidth <= innerWidth
|
|
51
|
+
? `${left}${" ".repeat(innerWidth - leftWidth - rightWidth)}${right}`
|
|
52
|
+
: truncateToWidth(left, innerWidth, "");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function composeFooterContent(
|
|
56
|
+
builtInLeft: string,
|
|
57
|
+
builtInRight: string,
|
|
58
|
+
extensionLeft: string[],
|
|
59
|
+
extensionMiddle: string[],
|
|
60
|
+
extensionRight: string[],
|
|
61
|
+
separator: string,
|
|
62
|
+
innerWidth: number,
|
|
63
|
+
): string {
|
|
64
|
+
const builtInLeftWidth = visibleWidth(builtInLeft);
|
|
65
|
+
const builtInRightWidth = visibleWidth(builtInRight);
|
|
66
|
+
const minimumGap = builtInLeft && builtInRight ? 1 : 0;
|
|
67
|
+
|
|
68
|
+
if (builtInLeftWidth + minimumGap + builtInRightWidth > innerWidth) {
|
|
69
|
+
return composeBuiltInFooterContent(builtInLeft, builtInRight, innerWidth);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const available = Math.max(0, innerWidth - builtInLeftWidth - builtInRightWidth - minimumGap);
|
|
73
|
+
let remaining = available;
|
|
74
|
+
const leftConnectorWidth = builtInLeft && extensionLeft.length > 0 ? visibleWidth(separator) : 0;
|
|
75
|
+
const rightConnectorWidth =
|
|
76
|
+
builtInRight && extensionRight.length > 0 ? visibleWidth(separator) : 0;
|
|
77
|
+
let leftStatus = "";
|
|
78
|
+
let rightStatus = "";
|
|
79
|
+
|
|
80
|
+
if (extensionLeft.length > 0 && extensionRight.length > 0) {
|
|
81
|
+
const leftBudget = Math.max(0, Math.floor(available / 2) - leftConnectorWidth);
|
|
82
|
+
leftStatus = fitStatusTexts(extensionLeft, leftBudget, separator);
|
|
83
|
+
remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
84
|
+
|
|
85
|
+
const rightBudget = Math.max(0, remaining - rightConnectorWidth);
|
|
86
|
+
rightStatus = fitStatusTexts(extensionRight, rightBudget, separator);
|
|
87
|
+
remaining -= rightStatus ? rightConnectorWidth + visibleWidth(rightStatus) : 0;
|
|
88
|
+
|
|
89
|
+
const expandedLeftBudget = Math.max(0, remaining + visibleWidth(leftStatus));
|
|
90
|
+
const expandedLeftStatus = fitStatusTexts(extensionLeft, expandedLeftBudget, separator);
|
|
91
|
+
if (visibleWidth(expandedLeftStatus) > visibleWidth(leftStatus)) {
|
|
92
|
+
remaining += leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
93
|
+
leftStatus = expandedLeftStatus;
|
|
94
|
+
remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
95
|
+
}
|
|
96
|
+
} else if (extensionLeft.length > 0) {
|
|
97
|
+
leftStatus = fitStatusTexts(
|
|
98
|
+
extensionLeft,
|
|
99
|
+
Math.max(0, available - leftConnectorWidth),
|
|
100
|
+
separator,
|
|
101
|
+
);
|
|
102
|
+
remaining -= leftStatus ? leftConnectorWidth + visibleWidth(leftStatus) : 0;
|
|
103
|
+
} else if (extensionRight.length > 0) {
|
|
104
|
+
rightStatus = fitStatusTexts(
|
|
105
|
+
extensionRight,
|
|
106
|
+
Math.max(0, available - rightConnectorWidth),
|
|
107
|
+
separator,
|
|
108
|
+
);
|
|
109
|
+
remaining -= rightStatus ? rightConnectorWidth + visibleWidth(rightStatus) : 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const left = appendStatusArea(builtInLeft, leftStatus, separator);
|
|
113
|
+
const right = prependStatusArea(builtInRight, rightStatus, separator);
|
|
114
|
+
const gapWidth = Math.max(0, innerWidth - visibleWidth(left) - visibleWidth(right));
|
|
115
|
+
const middle = fitStatusTexts(extensionMiddle, gapWidth, separator);
|
|
116
|
+
const middleWidth = visibleWidth(middle);
|
|
117
|
+
|
|
118
|
+
if (!middle || middleWidth <= 0) {
|
|
119
|
+
return `${left}${" ".repeat(gapWidth)}${right}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const leftPadding = Math.floor((gapWidth - middleWidth) / 2);
|
|
123
|
+
const rightPadding = gapWidth - middleWidth - leftPadding;
|
|
124
|
+
return `${left}${" ".repeat(leftPadding)}${middle}${" ".repeat(rightPadding)}${right}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
8
127
|
export function installFooter(
|
|
9
128
|
ctx: ExtensionContext,
|
|
10
129
|
state: FooterState,
|
|
@@ -12,10 +131,12 @@ export function installFooter(
|
|
|
12
131
|
hooks: {
|
|
13
132
|
setRequestRender: (fn: (() => void) | undefined) => void;
|
|
14
133
|
scheduleProjectRefresh: (ctx: ExtensionContext) => void;
|
|
134
|
+
setExtensionStatusesGetter?: (fn: (() => ReadonlyMap<string, string>) | undefined) => void;
|
|
15
135
|
},
|
|
16
136
|
): void {
|
|
17
137
|
ctx.ui.setFooter((tui, theme, footerData) => {
|
|
18
138
|
hooks.setRequestRender(() => tui.requestRender());
|
|
139
|
+
hooks.setExtensionStatusesGetter?.(() => footerData.getExtensionStatuses());
|
|
19
140
|
const unsubscribeBranch = footerData.onBranchChange(() => {
|
|
20
141
|
hooks.scheduleProjectRefresh(ctx);
|
|
21
142
|
tui.requestRender();
|
|
@@ -25,6 +146,7 @@ export function installFooter(
|
|
|
25
146
|
dispose: () => {
|
|
26
147
|
unsubscribeBranch();
|
|
27
148
|
hooks.setRequestRender(undefined);
|
|
149
|
+
hooks.setExtensionStatusesGetter?.(undefined);
|
|
28
150
|
},
|
|
29
151
|
invalidate() {},
|
|
30
152
|
render(width: number): string[] {
|
|
@@ -92,15 +214,21 @@ export function installFooter(
|
|
|
92
214
|
renderStyleForSource(theme, colorSource, config.colors.tokens, state.tokenLabel),
|
|
93
215
|
renderStyleForSource(theme, colorSource, config.colors.cost, state.costLabel),
|
|
94
216
|
].join(separator);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
217
|
+
const extensionStatuses = collectExtensionStatusSegments(
|
|
218
|
+
footerData.getExtensionStatuses(),
|
|
219
|
+
config,
|
|
220
|
+
);
|
|
221
|
+
const renderExtensionStatus = (text: string) =>
|
|
222
|
+
renderStyleForSource(theme, colorSource, config.colors.extensionStatus, text);
|
|
223
|
+
const content = composeFooterContent(
|
|
224
|
+
left,
|
|
225
|
+
right,
|
|
226
|
+
extensionStatuses.left.map((segment) => renderExtensionStatus(segment.text)),
|
|
227
|
+
extensionStatuses.middle.map((segment) => renderExtensionStatus(segment.text)),
|
|
228
|
+
extensionStatuses.right.map((segment) => renderExtensionStatus(segment.text)),
|
|
229
|
+
separator,
|
|
230
|
+
innerWidth,
|
|
231
|
+
);
|
|
104
232
|
const framed = width > 2 ? ` ${truncateToWidth(content, width - 2, "")} ` : content;
|
|
105
233
|
return [truncateToWidth(framed, width, "")];
|
|
106
234
|
},
|
|
@@ -7,10 +7,12 @@ import type {
|
|
|
7
7
|
import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
|
|
8
8
|
import {
|
|
9
9
|
type ColorSourcesConfig,
|
|
10
|
+
type ExtensionStatusPlacement,
|
|
10
11
|
type PolishedTuiConfig,
|
|
11
12
|
ensureConfigExists,
|
|
12
13
|
loadConfig,
|
|
13
14
|
saveColorSourcesPatch,
|
|
15
|
+
saveExtensionStatusPlacement,
|
|
14
16
|
} from "./config";
|
|
15
17
|
import { installFooter } from "./footer";
|
|
16
18
|
import { emptyGitStatus, readGitStatus } from "./git";
|
|
@@ -19,7 +21,6 @@ import { readRuntimeInfo } from "./runtime";
|
|
|
19
21
|
import { installSelectorBorderStyle } from "./selector-border";
|
|
20
22
|
import { registerZentuiSettingsCommand } from "./settings-command";
|
|
21
23
|
import { type FooterState, createInitialState, syncState } from "./state";
|
|
22
|
-
import { renderAccentLine, safeThemeFg } from "./style";
|
|
23
24
|
import { PolishedEditor } from "./ui";
|
|
24
25
|
import { installUserMessageStyle } from "./user-message";
|
|
25
26
|
|
|
@@ -29,6 +30,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
29
30
|
let currentConfig: PolishedTuiConfig = loadConfig();
|
|
30
31
|
let activeTheme: Theme | undefined;
|
|
31
32
|
let requestFooterRender: (() => void) | undefined;
|
|
33
|
+
let getActiveExtensionStatuses: () => ReadonlyMap<string, string> = () => new Map();
|
|
32
34
|
let stopRefreshInterval: StopProjectRefreshInterval = () => {};
|
|
33
35
|
let cleanupPrototypePatches: () => void = () => {};
|
|
34
36
|
let projectRefreshInFlight = false;
|
|
@@ -81,15 +83,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
81
83
|
keybindings,
|
|
82
84
|
ctx.ui.theme,
|
|
83
85
|
getCurrentConfig,
|
|
84
|
-
() =>
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
getCurrentConfig().colorSources.editor,
|
|
89
|
-
state.modelLabel,
|
|
90
|
-
),
|
|
91
|
-
safeThemeFg(ctx.ui.theme, "text", state.providerLabel),
|
|
92
|
-
].join(safeThemeFg(ctx.ui.theme, "borderMuted", " ")),
|
|
86
|
+
() => ({
|
|
87
|
+
modelLabel: state.modelLabel,
|
|
88
|
+
providerLabel: state.providerLabel,
|
|
89
|
+
}),
|
|
93
90
|
getThinkingLevel,
|
|
94
91
|
),
|
|
95
92
|
);
|
|
@@ -115,6 +112,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
115
112
|
requestFooterRender = fn;
|
|
116
113
|
},
|
|
117
114
|
scheduleProjectRefresh,
|
|
115
|
+
setExtensionStatusesGetter(fn) {
|
|
116
|
+
getActiveExtensionStatuses = fn ?? (() => new Map());
|
|
117
|
+
},
|
|
118
118
|
});
|
|
119
119
|
installEditor(ctx);
|
|
120
120
|
stopRefreshInterval = startProjectRefreshInterval(currentConfig.projectRefreshIntervalMs, () =>
|
|
@@ -132,6 +132,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
132
132
|
projectRefreshInFlight = false;
|
|
133
133
|
projectRefreshPending = false;
|
|
134
134
|
requestFooterRender = undefined;
|
|
135
|
+
getActiveExtensionStatuses = () => new Map();
|
|
135
136
|
if (ctx?.hasUI) {
|
|
136
137
|
ctx.ui.setFooter(undefined);
|
|
137
138
|
ctx.ui.setEditorComponent(undefined);
|
|
@@ -148,6 +149,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
148
149
|
setColorSources(patch: Partial<ColorSourcesConfig>) {
|
|
149
150
|
currentConfig = saveColorSourcesPatch(patch);
|
|
150
151
|
},
|
|
152
|
+
getActiveExtensionStatuses() {
|
|
153
|
+
return getActiveExtensionStatuses();
|
|
154
|
+
},
|
|
155
|
+
setExtensionStatusPlacement(key: string, placement: ExtensionStatusPlacement) {
|
|
156
|
+
currentConfig = saveExtensionStatusPlacement(key, placement);
|
|
157
|
+
},
|
|
151
158
|
requestRender() {
|
|
152
159
|
refresh();
|
|
153
160
|
},
|
|
@@ -6,16 +6,32 @@ import {
|
|
|
6
6
|
type SettingsListTheme,
|
|
7
7
|
truncateToWidth,
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
|
-
import
|
|
9
|
+
import {
|
|
10
|
+
type ColorSource,
|
|
11
|
+
type ColorSourcesConfig,
|
|
12
|
+
type ExtensionStatusPlacement,
|
|
13
|
+
type PolishedTuiConfig,
|
|
14
|
+
getExtensionStatusPlacement,
|
|
15
|
+
isExtensionStatusPlacement,
|
|
16
|
+
} from "./config";
|
|
17
|
+
import { sanitizeExtensionStatusText } from "./extension-status";
|
|
10
18
|
import { EDITOR_BORDER_STYLE, renderChromeBorder, safeThemeFg } from "./style";
|
|
11
19
|
|
|
12
20
|
const colorSourceValues: ColorSource[] = ["theme", "terminal"];
|
|
21
|
+
const extensionStatusPlacementValues: ExtensionStatusPlacement[] = [
|
|
22
|
+
"off",
|
|
23
|
+
"left",
|
|
24
|
+
"middle",
|
|
25
|
+
"right",
|
|
26
|
+
];
|
|
13
27
|
|
|
14
28
|
type SettingId = "starship" | "editorMessages";
|
|
15
29
|
|
|
16
30
|
type SettingsCommandDeps = {
|
|
17
31
|
getConfig: () => PolishedTuiConfig;
|
|
18
32
|
setColorSources: (patch: Partial<ColorSourcesConfig>) => void;
|
|
33
|
+
getActiveExtensionStatuses: () => ReadonlyMap<string, string>;
|
|
34
|
+
setExtensionStatusPlacement: (key: string, placement: ExtensionStatusPlacement) => void;
|
|
19
35
|
requestRender: () => void;
|
|
20
36
|
settingsListTheme?: SettingsListTheme;
|
|
21
37
|
};
|
|
@@ -50,27 +66,114 @@ function patchForSetting(id: SettingId, value: ColorSource): Partial<ColorSource
|
|
|
50
66
|
return id === "starship" ? { starship: value } : { editor: value, userMessages: value };
|
|
51
67
|
}
|
|
52
68
|
|
|
53
|
-
function buildItems(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
function buildItems(
|
|
70
|
+
config: PolishedTuiConfig,
|
|
71
|
+
activeStatusCount: number,
|
|
72
|
+
thirdPartyStatusesSubmenu: SettingItem["submenu"],
|
|
73
|
+
): SettingItem[] {
|
|
74
|
+
return [
|
|
75
|
+
...(Object.keys(settingLabels) as SettingId[]).map((key) => ({
|
|
76
|
+
id: key,
|
|
77
|
+
label: settingLabels[key],
|
|
78
|
+
description: settingDescriptions[key],
|
|
79
|
+
currentValue: key === "starship" ? config.colorSources.starship : editorMessageValue(config),
|
|
80
|
+
values: colorSourceValues,
|
|
81
|
+
})),
|
|
82
|
+
{
|
|
83
|
+
id: "thirdPartyStatuses",
|
|
84
|
+
label: "Third-party statuses",
|
|
85
|
+
description:
|
|
86
|
+
"Configure active ctx.ui.setStatus() footer statuses. Only currently active keys are listed.",
|
|
87
|
+
currentValue: `${activeStatusCount} active`,
|
|
88
|
+
submenu: thirdPartyStatusesSubmenu,
|
|
89
|
+
},
|
|
90
|
+
];
|
|
61
91
|
}
|
|
62
92
|
|
|
63
93
|
export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCommandDeps): void {
|
|
64
94
|
pi.registerCommand("zentui", {
|
|
65
|
-
description: "Configure Zentui
|
|
95
|
+
description: "Configure Zentui",
|
|
66
96
|
handler: async (_args, ctx) => {
|
|
67
97
|
if (!ctx.hasUI) return;
|
|
68
98
|
|
|
69
99
|
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
100
|
+
const settingsListTheme = deps.settingsListTheme ?? getSettingsListTheme();
|
|
101
|
+
const makeThirdPartyStatusesSubmenu: SettingItem["submenu"] = (_currentValue, close) => {
|
|
102
|
+
const activeStatuses = Array.from(deps.getActiveExtensionStatuses().entries()).sort(
|
|
103
|
+
([a], [b]) => (a < b ? -1 : a > b ? 1 : 0),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (activeStatuses.length === 0) {
|
|
107
|
+
return {
|
|
108
|
+
render(width: number) {
|
|
109
|
+
return [
|
|
110
|
+
truncateToWidth(safeThemeFg(theme, "accent", "Third-party statuses"), width, ""),
|
|
111
|
+
"",
|
|
112
|
+
truncateToWidth(
|
|
113
|
+
safeThemeFg(theme, "muted", "No third-party statuses are active."),
|
|
114
|
+
width,
|
|
115
|
+
"",
|
|
116
|
+
),
|
|
117
|
+
truncateToWidth(
|
|
118
|
+
safeThemeFg(
|
|
119
|
+
theme,
|
|
120
|
+
"muted",
|
|
121
|
+
"This menu only lists statuses currently published through ctx.ui.setStatus().",
|
|
122
|
+
),
|
|
123
|
+
width,
|
|
124
|
+
"",
|
|
125
|
+
),
|
|
126
|
+
"",
|
|
127
|
+
truncateToWidth(safeThemeFg(theme, "muted", "Esc to go back"), width, ""),
|
|
128
|
+
];
|
|
129
|
+
},
|
|
130
|
+
invalidate() {},
|
|
131
|
+
handleInput(data: string) {
|
|
132
|
+
if (data === "\x1b" || data === "\u0003") close(undefined);
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const statusItems: SettingItem[] = activeStatuses.map(([key, value]) => {
|
|
138
|
+
const sanitizedText = sanitizeExtensionStatusText(value);
|
|
139
|
+
return {
|
|
140
|
+
id: key,
|
|
141
|
+
label: key,
|
|
142
|
+
description: sanitizedText ? `Current status: ${sanitizedText}` : undefined,
|
|
143
|
+
currentValue: getExtensionStatusPlacement(deps.getConfig(), key),
|
|
144
|
+
values: extensionStatusPlacementValues,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
147
|
+
const statusSettingsList = new SettingsList(
|
|
148
|
+
statusItems,
|
|
149
|
+
8,
|
|
150
|
+
settingsListTheme,
|
|
151
|
+
(key, newValue) => {
|
|
152
|
+
if (!isExtensionStatusPlacement(newValue)) return;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
deps.setExtensionStatusPlacement(key, newValue);
|
|
156
|
+
statusSettingsList.updateValue(key, newValue);
|
|
157
|
+
deps.requestRender();
|
|
158
|
+
ctx.ui.notify(`Third-party status ${key}: ${newValue}`, "info");
|
|
159
|
+
tui.requestRender();
|
|
160
|
+
} catch (error) {
|
|
161
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
162
|
+
ctx.ui.notify(`Could not update Zentui settings: ${message}`, "error");
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
() => close(undefined),
|
|
166
|
+
);
|
|
167
|
+
return statusSettingsList;
|
|
168
|
+
};
|
|
70
169
|
const settingsList = new SettingsList(
|
|
71
|
-
buildItems(
|
|
170
|
+
buildItems(
|
|
171
|
+
deps.getConfig(),
|
|
172
|
+
deps.getActiveExtensionStatuses().size,
|
|
173
|
+
makeThirdPartyStatusesSubmenu,
|
|
174
|
+
),
|
|
72
175
|
5,
|
|
73
|
-
|
|
176
|
+
settingsListTheme,
|
|
74
177
|
(id, newValue) => {
|
|
75
178
|
if (!isSettingId(id) || !isColorSource(newValue)) return;
|
|
76
179
|
|
|
@@ -13,6 +13,21 @@ export type { ThemeLike };
|
|
|
13
13
|
export const EDITOR_ACCENT_STYLE = "blue";
|
|
14
14
|
export const EDITOR_BORDER_STYLE = "bright-black";
|
|
15
15
|
|
|
16
|
+
export type SourceStyleFallback = {
|
|
17
|
+
theme: ColorSpec;
|
|
18
|
+
terminal: ColorSpec;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const EDITOR_ACCENT_FALLBACK: SourceStyleFallback = {
|
|
22
|
+
theme: "accent",
|
|
23
|
+
terminal: EDITOR_ACCENT_STYLE,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const EDITOR_BORDER_FALLBACK: SourceStyleFallback = {
|
|
27
|
+
theme: "borderMuted",
|
|
28
|
+
terminal: EDITOR_BORDER_STYLE,
|
|
29
|
+
};
|
|
30
|
+
|
|
16
31
|
function isHexColor(value: string): boolean {
|
|
17
32
|
return /^#(?:[0-9a-fA-F]{6})$/.test(value);
|
|
18
33
|
}
|
|
@@ -267,6 +282,17 @@ export function renderStyleForSource(
|
|
|
267
282
|
: renderThemeStyle(theme, style, text);
|
|
268
283
|
}
|
|
269
284
|
|
|
285
|
+
export function renderStyleForSourceOrFallback(
|
|
286
|
+
theme: ThemeLike,
|
|
287
|
+
source: ColorSource,
|
|
288
|
+
style: ColorSpec | undefined,
|
|
289
|
+
fallback: ColorSpec | SourceStyleFallback,
|
|
290
|
+
text: string,
|
|
291
|
+
): string {
|
|
292
|
+
const fallbackStyle = typeof fallback === "string" ? fallback : fallback[source];
|
|
293
|
+
return renderStyleForSource(theme, source, style ?? fallbackStyle, text);
|
|
294
|
+
}
|
|
295
|
+
|
|
270
296
|
export function renderEditorAccent(text: string): string {
|
|
271
297
|
return renderTerminalStyle(EDITOR_ACCENT_STYLE, text);
|
|
272
298
|
}
|
|
@@ -276,7 +302,7 @@ export function renderEditorBorder(text: string): string {
|
|
|
276
302
|
}
|
|
277
303
|
|
|
278
304
|
export function renderAccentLine(theme: ThemeLike, source: ColorSource, text: string): string {
|
|
279
|
-
return
|
|
305
|
+
return renderStyleForSourceOrFallback(theme, source, undefined, EDITOR_ACCENT_FALLBACK, text);
|
|
280
306
|
}
|
|
281
307
|
|
|
282
308
|
export function renderChromeBorder(
|
|
@@ -285,6 +311,11 @@ export function renderChromeBorder(
|
|
|
285
311
|
terminalFallbackStyle: ColorSpec,
|
|
286
312
|
text: string,
|
|
287
313
|
): string {
|
|
288
|
-
|
|
289
|
-
|
|
314
|
+
return renderStyleForSourceOrFallback(
|
|
315
|
+
theme,
|
|
316
|
+
source,
|
|
317
|
+
undefined,
|
|
318
|
+
{ theme: "borderMuted", terminal: terminalFallbackStyle },
|
|
319
|
+
text,
|
|
320
|
+
);
|
|
290
321
|
}
|
package/extensions/zentui/ui.ts
CHANGED
|
@@ -7,20 +7,30 @@ import {
|
|
|
7
7
|
visibleWidth,
|
|
8
8
|
} from "@earendil-works/pi-tui";
|
|
9
9
|
import type { PolishedTuiConfig } from "./config";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
EDITOR_ACCENT_FALLBACK,
|
|
12
|
+
EDITOR_BORDER_FALLBACK,
|
|
13
|
+
renderStyleForSourceOrFallback,
|
|
14
|
+
safeThemeFg,
|
|
15
|
+
} from "./style";
|
|
11
16
|
|
|
12
17
|
type AutocompleteEditorInternals = {
|
|
13
18
|
autocompleteList?: Pick<Component, "render">;
|
|
14
19
|
isShowingAutocomplete?: () => boolean;
|
|
15
20
|
};
|
|
16
21
|
|
|
22
|
+
type EditorMeta = {
|
|
23
|
+
modelLabel: string;
|
|
24
|
+
providerLabel: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
17
27
|
function clampRenderedLines(lines: string[], width: number): string[] {
|
|
18
28
|
const maxWidth = Math.max(0, width);
|
|
19
29
|
return lines.map((line) => truncateToWidth(line, maxWidth, ""));
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
export class PolishedEditor extends CustomEditor {
|
|
23
|
-
private readonly getModelMeta: () =>
|
|
33
|
+
private readonly getModelMeta: () => EditorMeta;
|
|
24
34
|
private readonly getThinkingLevel: () => string | undefined;
|
|
25
35
|
private readonly getConfig: () => PolishedTuiConfig;
|
|
26
36
|
private readonly uiTheme: Theme;
|
|
@@ -32,7 +42,7 @@ export class PolishedEditor extends CustomEditor {
|
|
|
32
42
|
keybindings: KeybindingsManager,
|
|
33
43
|
uiTheme: Theme,
|
|
34
44
|
getConfig: () => PolishedTuiConfig,
|
|
35
|
-
getModelMeta: () =>
|
|
45
|
+
getModelMeta: () => EditorMeta,
|
|
36
46
|
getThinkingLevel: () => string | undefined,
|
|
37
47
|
) {
|
|
38
48
|
super(tui, theme, keybindings, { paddingX: 0 });
|
|
@@ -49,6 +59,23 @@ export class PolishedEditor extends CustomEditor {
|
|
|
49
59
|
return `${truncated}${pad}`;
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
private editorThinkingStyle(config: PolishedTuiConfig, level: string): string | undefined {
|
|
63
|
+
switch (level.toLowerCase()) {
|
|
64
|
+
case "minimal":
|
|
65
|
+
return config.colors.editorThinkingMinimal ?? config.colors.editorThinking;
|
|
66
|
+
case "low":
|
|
67
|
+
return config.colors.editorThinkingLow ?? config.colors.editorThinking;
|
|
68
|
+
case "medium":
|
|
69
|
+
return config.colors.editorThinkingMedium ?? config.colors.editorThinking;
|
|
70
|
+
case "high":
|
|
71
|
+
return config.colors.editorThinkingHigh ?? config.colors.editorThinking;
|
|
72
|
+
case "xhigh":
|
|
73
|
+
return config.colors.editorThinkingXhigh ?? config.colors.editorThinking;
|
|
74
|
+
default:
|
|
75
|
+
return config.colors.editorThinking;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
52
79
|
render(width: number): string[] {
|
|
53
80
|
if (width <= 2) {
|
|
54
81
|
return clampRenderedLines(super.render(width), width);
|
|
@@ -84,26 +111,61 @@ export class PolishedEditor extends CustomEditor {
|
|
|
84
111
|
return clampRenderedLines(rendered, width);
|
|
85
112
|
}
|
|
86
113
|
|
|
114
|
+
const config = this.getConfig();
|
|
115
|
+
const colorSource = config.colorSources.editor;
|
|
87
116
|
const editorLines = editorFrame.slice(1, -1);
|
|
88
|
-
const
|
|
117
|
+
const { modelLabel, providerLabel } = this.getModelMeta();
|
|
118
|
+
const model = renderStyleForSourceOrFallback(
|
|
119
|
+
this.uiTheme,
|
|
120
|
+
colorSource,
|
|
121
|
+
config.colors.editorModel,
|
|
122
|
+
EDITOR_ACCENT_FALLBACK,
|
|
123
|
+
modelLabel,
|
|
124
|
+
);
|
|
125
|
+
const provider = renderStyleForSourceOrFallback(
|
|
126
|
+
this.uiTheme,
|
|
127
|
+
colorSource,
|
|
128
|
+
config.colors.editorProvider,
|
|
129
|
+
"text",
|
|
130
|
+
providerLabel,
|
|
131
|
+
);
|
|
132
|
+
const modelMeta = [model, provider]
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.join(safeThemeFg(this.uiTheme, "borderMuted", " "));
|
|
135
|
+
const metaParts = [modelMeta];
|
|
89
136
|
const thinkingLevel = this.getThinkingLevel();
|
|
90
137
|
if (thinkingLevel && thinkingLevel !== "off") {
|
|
91
|
-
metaParts.push(
|
|
138
|
+
metaParts.push(
|
|
139
|
+
renderStyleForSourceOrFallback(
|
|
140
|
+
this.uiTheme,
|
|
141
|
+
colorSource,
|
|
142
|
+
this.editorThinkingStyle(config, thinkingLevel),
|
|
143
|
+
"muted",
|
|
144
|
+
thinkingLevel,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
92
147
|
}
|
|
93
148
|
const meta = metaParts.filter(Boolean).join(safeThemeFg(this.uiTheme, "border", " "));
|
|
94
149
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
150
|
+
const rail = `${renderStyleForSourceOrFallback(
|
|
151
|
+
this.uiTheme,
|
|
152
|
+
colorSource,
|
|
153
|
+
config.colors.editorAccent,
|
|
154
|
+
EDITOR_ACCENT_FALLBACK,
|
|
155
|
+
"│",
|
|
156
|
+
)}${this.reset} `;
|
|
157
|
+
const top = renderStyleForSourceOrFallback(
|
|
98
158
|
this.uiTheme,
|
|
99
159
|
colorSource,
|
|
100
|
-
|
|
160
|
+
config.colors.editorBorder,
|
|
161
|
+
EDITOR_BORDER_FALLBACK,
|
|
101
162
|
"─".repeat(width),
|
|
102
163
|
);
|
|
103
|
-
const bottom =
|
|
164
|
+
const bottom = renderStyleForSourceOrFallback(
|
|
104
165
|
this.uiTheme,
|
|
105
166
|
colorSource,
|
|
106
|
-
|
|
167
|
+
config.colors.editorBorder,
|
|
168
|
+
EDITOR_BORDER_FALLBACK,
|
|
107
169
|
"─".repeat(width),
|
|
108
170
|
);
|
|
109
171
|
const lines = ["", ...editorLines, "", meta];
|
|
@@ -6,7 +6,11 @@ import {
|
|
|
6
6
|
visibleWidth,
|
|
7
7
|
} from "@earendil-works/pi-tui";
|
|
8
8
|
import type { PolishedTuiConfig } from "./config";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
EDITOR_ACCENT_FALLBACK,
|
|
11
|
+
EDITOR_BORDER_FALLBACK,
|
|
12
|
+
renderStyleForSourceOrFallback,
|
|
13
|
+
} from "./style";
|
|
10
14
|
|
|
11
15
|
const OSC133_ZONE_START = "\x1b]133;A\x07";
|
|
12
16
|
const OSC133_ZONE_END = "\x1b]133;B\x07";
|
|
@@ -92,7 +96,17 @@ function renderPromptBoxLine(
|
|
|
92
96
|
config: PolishedTuiConfig,
|
|
93
97
|
): string {
|
|
94
98
|
if (width <= 0) return "";
|
|
95
|
-
const rail = `${
|
|
99
|
+
const rail = `${
|
|
100
|
+
theme
|
|
101
|
+
? renderStyleForSourceOrFallback(
|
|
102
|
+
theme,
|
|
103
|
+
config.colorSources.userMessages,
|
|
104
|
+
config.colors.editorAccent,
|
|
105
|
+
EDITOR_ACCENT_FALLBACK,
|
|
106
|
+
"│",
|
|
107
|
+
)
|
|
108
|
+
: "│"
|
|
109
|
+
} `;
|
|
96
110
|
const contentWidth = Math.max(0, width - visibleWidth(rail));
|
|
97
111
|
return truncateToWidth(`${rail}${fillLine(line, contentWidth)}`, width, "");
|
|
98
112
|
}
|
|
@@ -108,7 +122,17 @@ function renderZentuiUserMessage(
|
|
|
108
122
|
if (width <= 0) return [""];
|
|
109
123
|
|
|
110
124
|
const railWidth = visibleWidth(
|
|
111
|
-
`${
|
|
125
|
+
`${
|
|
126
|
+
theme
|
|
127
|
+
? renderStyleForSourceOrFallback(
|
|
128
|
+
theme,
|
|
129
|
+
config.colorSources.userMessages,
|
|
130
|
+
config.colors.editorAccent,
|
|
131
|
+
EDITOR_ACCENT_FALLBACK,
|
|
132
|
+
"│",
|
|
133
|
+
)
|
|
134
|
+
: "│"
|
|
135
|
+
} `,
|
|
112
136
|
);
|
|
113
137
|
const contentWidth = Math.max(1, width - railWidth);
|
|
114
138
|
const renderer = new Markdown(text, 0, 0, makeMarkdownTheme(theme), {
|
|
@@ -117,10 +141,11 @@ function renderZentuiUserMessage(
|
|
|
117
141
|
const body = renderer.render(contentWidth);
|
|
118
142
|
const contentLines = body.length > 0 ? body : [""];
|
|
119
143
|
const border = theme
|
|
120
|
-
?
|
|
144
|
+
? renderStyleForSourceOrFallback(
|
|
121
145
|
theme,
|
|
122
146
|
config.colorSources.userMessages,
|
|
123
|
-
|
|
147
|
+
config.colors.editorBorder,
|
|
148
|
+
EDITOR_BORDER_FALLBACK,
|
|
124
149
|
"─".repeat(width),
|
|
125
150
|
)
|
|
126
151
|
: "─".repeat(width);
|