@thoth-agents/pi-thoth-theme 0.0.0-stage → 0.1.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.
Files changed (42) hide show
  1. package/LICENSE +29 -0
  2. package/README.md +115 -2
  3. package/package.json +63 -3
  4. package/src/index.ts +53 -0
  5. package/src/input-box/decorate.ts +304 -0
  6. package/src/input-box/frame.ts +182 -0
  7. package/src/input-box/gradient.ts +92 -0
  8. package/src/input-box/state.ts +90 -0
  9. package/src/render-kit/index.ts +305 -0
  10. package/src/render-kit/working.ts +25 -0
  11. package/src/shared/cache.ts +21 -0
  12. package/src/shared/config.ts +86 -0
  13. package/src/shared/duration.ts +1 -0
  14. package/src/shared/icons.ts +89 -0
  15. package/src/status-line/cost.ts +56 -0
  16. package/src/status-line/index.ts +242 -0
  17. package/src/status-line/layout.ts +272 -0
  18. package/src/status-line/snapshot.ts +11 -0
  19. package/src/status-line/throughput.ts +61 -0
  20. package/src/status-line/tokens.ts +51 -0
  21. package/src/tools/bash.ts +237 -0
  22. package/src/tools/border.ts +21 -0
  23. package/src/tools/box.ts +258 -0
  24. package/src/tools/edit.ts +228 -0
  25. package/src/tools/file-icons.ts +139 -0
  26. package/src/tools/find.ts +227 -0
  27. package/src/tools/frame.ts +166 -0
  28. package/src/tools/generic.ts +195 -0
  29. package/src/tools/grep.ts +371 -0
  30. package/src/tools/index.ts +135 -0
  31. package/src/tools/ls.ts +231 -0
  32. package/src/tools/ownership.ts +70 -0
  33. package/src/tools/powershell.ts +1 -0
  34. package/src/tools/read.ts +194 -0
  35. package/src/tools/ticker.ts +101 -0
  36. package/src/tools/write.ts +188 -0
  37. package/src/welcome/age.ts +11 -0
  38. package/src/welcome/index.ts +132 -0
  39. package/src/welcome/logo.ts +35 -0
  40. package/src/welcome/render.ts +323 -0
  41. package/src/welcome/resources.ts +251 -0
  42. package/themes/thoth.json +104 -0
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 thoth-agents contributors
4
+ Copyright (c) 2026 QuangThai
5
+ Copyright (c) 2025 huynhgiabuu
6
+ Copyright (c) 2026 Michael
7
+ Copyright (c) 2025 Mario Zechner
8
+
9
+ Includes code adapted from pi-omp-theme (QuangThai), the primary source, and
10
+ pi-pretty (huynhgiabuu). pi-atelier (Michael) and gentle-shell (Mario Zechner)
11
+ are reference-only projects; their code is not included in this package.
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,116 @@
1
- # Temporary Holding Version
1
+ # @thoth-agents/pi-thoth-theme
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Modular Thoth styling for Pi, with a gold-on-black Egyptian dark theme.
4
+
5
+ Requires Pi `>=0.99.0` and Node `>=22.19.0`. Development Pi SDK/TUI
6
+ dependencies are pinned to `1.0.2` in the root workspace lockfile.
7
+
8
+ ## Install
9
+
10
+ From the thoth-agents repository root:
11
+
12
+ ```sh
13
+ pi install ./pi-packages/pi-thoth-theme
14
+ ```
15
+
16
+ Select the `thoth` theme in Pi's theme settings. Theme selection is independent
17
+ of the module toggles below.
18
+
19
+ ## Modules
20
+
21
+ - **Status line**: a responsive footer below the input showing cumulative
22
+ session cost (including subagents), main-session input/output totals, cache hit
23
+ percentage and output speed in `tok/s`. Input includes uncached, cache-read and
24
+ cache-write tokens; the hit percentage is cache-read divided by total input
25
+ (`—` when total input is zero). Subagent tokens are excluded. Narrow widths
26
+ drop speed, then cache, then tokens before truncating cost. Editor-border
27
+ metadata is decorated in place without claiming the custom editor slot.
28
+ - **Tools**: boxed calls and results for `read`, `bash`, `powershell`, `ls`,
29
+ `grep`, `find`, `edit` and `write`, with Nerd Font icons or ASCII
30
+ alternatives. `bash` and `powershell` show a live elapsed time while running. Execution and
31
+ parameters delegate to Pi's built-in tools; Pi's native output toggle still
32
+ collapses and expands results. Registered non-built-in tools keep their own
33
+ renderers when their package matches `tools.respectPackages` (including
34
+ thoth-agents and thoth-mem by default). Other tools get a generic frame with
35
+ the tool name, a one-line argument summary, a collapsed text preview, error
36
+ styling and a live elapsed footer. Images keep Pi's native rendering.
37
+ Tool renderers require Pi `>=1.0.1`; they are runtime-guarded and inert on
38
+ older supported Pi versions.
39
+ - **Welcome**: a header with the Thoth logo, Pi version, loaded resources and
40
+ tool providers, and recent sessions. Resource and session details load
41
+ best-effort through public APIs.
42
+
43
+ ## Render KIT
44
+
45
+ Provides the Render KIT v1 contract defined by `@thoth-agents/pi-core` for
46
+ first-party tools, custom messages and above-editor widgets. The kit shares this
47
+ package's frames, theme roles, collapse hints and working/elapsed indicators.
48
+ Producers discover it at render time, so extension load order does not matter;
49
+ they do not depend on this package and keep native Pi rendering when it is absent.
50
+
51
+ The theme registers the kit on `session_start` only in interactive UI sessions
52
+ with `tools.enabled: true`, and withdraws only its own registration on
53
+ `session_shutdown`. Headless children neither replace nor withdraw the parent
54
+ kit, and their lifecycle does not stop the parent's indicators. Theme selection
55
+ alone does not enable the kit when tool styling is disabled.
56
+
57
+ ## Configuration
58
+
59
+ Edit `~/.pi/agent/pi-thoth-theme.json` (or
60
+ `$PI_CODING_AGENT_DIR/pi-thoth-theme.json` when that environment variable is set):
61
+
62
+ ```json
63
+ {
64
+ "icons": "nerd",
65
+ "statusLine": {
66
+ "enabled": true,
67
+ "subscriptionProviders": ["claude-bridge"]
68
+ },
69
+ "inputBox": { "enabled": true },
70
+ "tools": {
71
+ "enabled": true,
72
+ "respectPackages": ["thoth-agents", "@thoth-agents/*", "thoth-mem"]
73
+ },
74
+ "welcome": { "enabled": true }
75
+ }
76
+ ```
77
+
78
+ `icons` accepts `"nerd"` (default) or `"ascii"`. Each module's `enabled` key
79
+ accepts a boolean and defaults to `true`. Set it to `false` to leave Pi's native
80
+ behavior on that surface. `statusLine.subscriptionProviders` accepts an array of
81
+ provider identifier strings (defaults to `["claude-bridge"]`) to mark
82
+ subscription-backed usage with `(sub)` in the status line cost segment.
83
+
84
+ `inputBox.enabled` defaults to `true` and requires `statusLine.enabled` to be
85
+ `true`. It frames the native editor in a rounded `muted` (sand) box with side borders,
86
+ a dim `type or / for commands` placeholder, and top-left ready or native working
87
+ status (including elapsed seconds). Git branch and working directory appear on
88
+ the top border; model, thinking effort and context usage appear on the bottom.
89
+ Native scroll indicators are preserved. The status line always stays in its
90
+ separate footer row below the box. Narrow widths use the native editor geometry.
91
+ Disable `inputBox` to keep the native editor and the same footer.
92
+
93
+ `tools.respectPackages` accepts an array of non-empty package-name strings.
94
+ The default is `["thoth-agents", "@thoth-agents/*", "thoth-mem"]`. Entries match
95
+ exact package names or a trailing `/*` scope wildcard: `@scope/*` matches
96
+ `@scope/anything`, but not `@scopex/anything`. No other globs are supported.
97
+ A user list **fully replaces** the default; `[]` gives every registered
98
+ non-built-in tool a generic frame, even if it supplies its own renderers.
99
+ Invalid values (including any empty, whitespace-only or non-string entry) use
100
+ the full default list. Package names come from the nearest `package.json`
101
+ walking upward from the tool's source directory. Built-ins always use the
102
+ themed frames; tools absent from Pi's registry keep their own renderers.
103
+
104
+ Configuration is read at extension load; use `/reload` after edits. Missing,
105
+ malformed or invalid values use defaults. Pi settings are never read for
106
+ package configuration.
107
+
108
+ **A Nerd Font is required for the default icons.** Configure your terminal to
109
+ use one, or set `"icons": "ascii"` for plain-text alternatives.
110
+
111
+ ## Attribution
112
+
113
+ MIT; see [LICENSE](LICENSE). Includes code adapted from the primary upstream,
114
+ [pi-omp-theme](https://github.com/QuangThai/pi-omp-theme) by QuangThai, and
115
+ pi-pretty by huynhgiabuu. pi-atelier by Michael and gentle-shell by Mario Zechner
116
+ are references only, not bundled code.
package/package.json CHANGED
@@ -1,6 +1,66 @@
1
1
  {
2
2
  "name": "@thoth-agents/pi-thoth-theme",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "description": "Modular Thoth theme, status line, tool styling, and welcome header for Pi.",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "engines": {
9
+ "node": ">=22.19.0"
10
+ },
11
+ "keywords": [
12
+ "pi-package",
13
+ "pi-extension",
14
+ "pi",
15
+ "theme"
16
+ ],
17
+ "pi": {
18
+ "extensions": [
19
+ "./src/index.ts"
20
+ ],
21
+ "themes": [
22
+ "./themes"
23
+ ]
24
+ },
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/EremesNG/thoth-agents.git",
28
+ "directory": "pi-packages/pi-thoth-theme"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "files": [
34
+ "src/**/*.ts",
35
+ "!src/**/*.test.ts",
36
+ "themes/**",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "peerDependencies": {
41
+ "@earendil-works/pi-coding-agent": ">=0.99.0",
42
+ "@earendil-works/pi-tui": ">=0.99.0",
43
+ "typebox": "*"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.10.0",
47
+ "typebox": "^1.3.4",
48
+ "typescript": "^5.8.0",
49
+ "vitest": "^4.1.9",
50
+ "@earendil-works/pi-coding-agent": "1.0.2",
51
+ "@earendil-works/pi-tui": "1.0.2",
52
+ "@earendil-works/pi-ai": "1.0.2",
53
+ "@rolldown/binding-wasm32-wasi": "^1.2.4"
54
+ },
55
+ "upstream": {
56
+ "repository": "https://github.com/QuangThai/pi-omp-theme",
57
+ "role": "primary source"
58
+ },
59
+ "dependencies": {
60
+ "@thoth-agents/pi-core": "^0.1.0"
61
+ },
62
+ "scripts": {
63
+ "typecheck": "tsc --noEmit",
64
+ "test": "vitest run"
65
+ }
6
66
  }
package/src/index.ts ADDED
@@ -0,0 +1,53 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import {
3
+ type RenderKitToken,
4
+ registerRenderKit,
5
+ withdrawRenderKit,
6
+ } from '@thoth-agents/pi-core';
7
+ import { createRenderKit } from './render-kit/index.ts';
8
+ import { loadConfig } from './shared/config.ts';
9
+ import { frames, icon } from './shared/icons.ts';
10
+ import { registerStatusLine } from './status-line/index.ts';
11
+ import { createToolRendererResolver, registerTools } from './tools/index.ts';
12
+ import { registerWelcome } from './welcome/index.ts';
13
+
14
+ export default function thothTheme(pi: ExtensionAPI): void {
15
+ const config = loadConfig();
16
+ const owner = {};
17
+ const cwd = process.cwd();
18
+ const resolveToolRenderers = config.tools.enabled
19
+ ? createToolRendererResolver(pi, config, cwd)
20
+ : undefined;
21
+ let kitToken: RenderKitToken | undefined;
22
+ if (resolveToolRenderers) {
23
+ registerTools(pi, config, cwd, owner, resolveToolRenderers);
24
+ }
25
+
26
+ pi.on('session_shutdown', (_event, ctx) => {
27
+ if (!ctx.hasUI || kitToken === undefined) return;
28
+ withdrawRenderKit(kitToken);
29
+ kitToken = undefined;
30
+ });
31
+
32
+ pi.on('session_start', (_event, ctx) => {
33
+ if (!ctx.hasUI) return;
34
+ if (config.tools.enabled) {
35
+ kitToken = registerRenderKit(
36
+ createRenderKit(owner, resolveToolRenderers, config.icons),
37
+ owner,
38
+ );
39
+ }
40
+ if (!ctx.ui) return;
41
+ if (config.statusLine.enabled) {
42
+ if (config.inputBox?.enabled !== false) {
43
+ ctx.ui.setWorkingIndicator?.({
44
+ frames: [...frames('workingFrames', config.icons)],
45
+ intervalMs: 200,
46
+ });
47
+ ctx.ui.setWorkingMessage?.(`working${icon('ellipsis', config.icons)}`);
48
+ }
49
+ registerStatusLine(pi, ctx, config);
50
+ }
51
+ if (config.welcome.enabled) registerWelcome(pi, ctx, config);
52
+ });
53
+ }
@@ -0,0 +1,304 @@
1
+ import {
2
+ type TuiMouseEvent,
3
+ type TuiMouseEventResult,
4
+ truncateToWidth,
5
+ visibleWidth,
6
+ } from '@earendil-works/pi-tui';
7
+ import type { IconMode } from '../shared/config.ts';
8
+ import {
9
+ type ActiveThemeLike,
10
+ formatBranchSegment,
11
+ formatContextSegments,
12
+ formatCwdSegments,
13
+ formatModelSegments,
14
+ } from '../status-line/layout.ts';
15
+ import type {
16
+ StatusSnapshot,
17
+ StatusSnapshotProvider,
18
+ } from '../status-line/snapshot.ts';
19
+ import {
20
+ inputLabelWidth,
21
+ renderInputBottom,
22
+ renderInputTop,
23
+ renderPlaceholder,
24
+ scrollLabel,
25
+ wrapContentRow,
26
+ } from './frame.ts';
27
+ import { type BreathingFrame, createBreathingFrame } from './gradient.ts';
28
+ import type { WorkingState } from './state.ts';
29
+
30
+ export const MIN_INPUT_BOX_WIDTH = 16;
31
+
32
+ interface EditorLike {
33
+ render(width: number): string[];
34
+ renderTopBorder(width: number, hiddenLineCount: number): string;
35
+ renderBottomBorder(width: number, hiddenLineCount: number): string;
36
+ getText(): string;
37
+ getPaddingX(): number;
38
+ handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined;
39
+ borderColor(text: string): string;
40
+ workingStatusIndicator?: unknown;
41
+ }
42
+
43
+ export interface InputBoxDeps {
44
+ theme: ActiveThemeLike;
45
+ working: WorkingState;
46
+ /** Omission retains Unicode glyphs; may be a live getter. */
47
+ readonly iconMode?: IconMode;
48
+ /** Shared live data; optional for standalone decorator consumers. */
49
+ getStatusSnapshot?: StatusSnapshotProvider;
50
+ }
51
+
52
+ export interface EditorDecoration {
53
+ getStatusSnapshot(): Readonly<StatusSnapshot> | undefined;
54
+ dispose(): void;
55
+ }
56
+
57
+ const decoratedEditors = new WeakSet<object>();
58
+ const decorations = new WeakMap<object, EditorDecoration>();
59
+ const methodNames = [
60
+ 'render',
61
+ 'renderTopBorder',
62
+ 'renderBottomBorder',
63
+ 'getText',
64
+ 'getPaddingX',
65
+ 'handleMouse',
66
+ 'borderColor',
67
+ ] as const;
68
+
69
+ // Do not read editor.render: the background-task Proxy getter dynamically calls
70
+ // target.render, which would recurse after replacing that same target method.
71
+ function method<Name extends keyof EditorLike>(
72
+ editor: object,
73
+ name: Name,
74
+ ): EditorLike[Name] | undefined {
75
+ for (
76
+ let owner: object | null = editor;
77
+ owner;
78
+ owner = Reflect.getPrototypeOf(owner)
79
+ ) {
80
+ const descriptor = Reflect.getOwnPropertyDescriptor(owner, name);
81
+ if (descriptor)
82
+ return typeof descriptor.value === 'function'
83
+ ? descriptor.value
84
+ : undefined;
85
+ }
86
+ return undefined;
87
+ }
88
+
89
+ export function isEditorLike(editor: unknown): editor is EditorLike {
90
+ if (!editor || typeof editor !== 'object') return false;
91
+ if (decoratedEditors.has(editor)) return true;
92
+ try {
93
+ return methodNames.every((name) => method(editor, name) !== undefined);
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ export function decorateEditor(
100
+ editor: unknown,
101
+ deps: InputBoxDeps,
102
+ ): EditorDecoration | undefined {
103
+ if (!isEditorLike(editor)) return undefined;
104
+ if (decoratedEditors.has(editor)) return decorations.get(editor);
105
+ const names = [
106
+ 'render',
107
+ 'renderTopBorder',
108
+ 'renderBottomBorder',
109
+ 'handleMouse',
110
+ 'borderColor',
111
+ ] as const;
112
+ if (
113
+ !Reflect.isExtensible(editor) ||
114
+ names.some(
115
+ (name) =>
116
+ Reflect.getOwnPropertyDescriptor(editor, name)?.configurable === false,
117
+ )
118
+ )
119
+ return undefined;
120
+ const originals = Object.fromEntries(
121
+ methodNames.map((name) => [name, method(editor, name)]),
122
+ ) as unknown as EditorLike;
123
+ const descriptors = new Map(
124
+ names.map((name) => [name, Reflect.getOwnPropertyDescriptor(editor, name)]),
125
+ );
126
+ let active = true;
127
+ let outerWidth: number | undefined;
128
+ let fallback = false;
129
+ let top: string | undefined;
130
+ let bottom: string | undefined;
131
+ let renderFrame: BreathingFrame | undefined;
132
+ // Mouse hit-testing follows the last render's boxed or native geometry.
133
+ let boxRendered = false;
134
+ const muted = (text: string) => deps.theme.fg?.('muted', text) ?? text;
135
+
136
+ function nativeRender(receiver: EditorLike, width: number): string[] {
137
+ fallback = true;
138
+ try {
139
+ return originals.render.call(receiver, width);
140
+ } finally {
141
+ fallback = false;
142
+ }
143
+ }
144
+
145
+ const patches: PropertyDescriptorMap = {
146
+ render: {
147
+ configurable: true,
148
+ writable: true,
149
+ value(this: EditorLike, width: number): string[] {
150
+ boxRendered = false;
151
+ if (!active || width < MIN_INPUT_BOX_WIDTH)
152
+ return nativeRender(this, width);
153
+ const boxWidth = Math.floor(width);
154
+ const innerWidth = boxWidth - 2;
155
+ const maxPadding = Math.max(0, Math.floor((innerWidth - 1) / 2));
156
+ const paddingX = Math.min(originals.getPaddingX.call(this), maxPadding);
157
+ const contentWidth = Math.max(1, innerWidth - paddingX * 2);
158
+ const layoutWidth = Math.max(1, contentWidth - (paddingX ? 0 : 1));
159
+ // Native word wrapping recurses on a wide grapheme at width 1. Keep
160
+ // its original geometry when the box plus current padding is unsafe.
161
+ if (layoutWidth < 2) return nativeRender(this, width);
162
+ // Share one time/color sample with the native status and every border.
163
+ const frame = deps.working.isWorking
164
+ ? createBreathingFrame(Date.now())
165
+ : undefined;
166
+ renderFrame = frame;
167
+ outerWidth = boxWidth;
168
+ top = undefined;
169
+ bottom = undefined;
170
+ let lines: string[];
171
+ try {
172
+ lines = originals.render.call(this, outerWidth - 2);
173
+ } finally {
174
+ outerWidth = undefined;
175
+ renderFrame = undefined;
176
+ }
177
+ const bottomIndex =
178
+ bottom === undefined ? -1 : lines.indexOf(bottom, 1);
179
+ if (top === undefined || lines[0] !== top || bottomIndex < 2)
180
+ return nativeRender(this, width);
181
+ const text = originals.getText.call(this);
182
+ lines = lines.map((line, index) => {
183
+ if (index === 0 || index === bottomIndex) return line;
184
+ if (index > bottomIndex)
185
+ return ` ${truncateToWidth(line, width - 2, '', true)} `;
186
+ return wrapContentRow(
187
+ renderPlaceholder(line, width - 2, deps.theme, text),
188
+ width,
189
+ deps.theme,
190
+ frame,
191
+ );
192
+ });
193
+ boxRendered = true;
194
+ return lines;
195
+ },
196
+ },
197
+ renderTopBorder: {
198
+ configurable: true,
199
+ writable: true,
200
+ value(this: EditorLike, width: number, hidden: number): string {
201
+ if (
202
+ outerWidth === undefined ||
203
+ (hidden > 0 &&
204
+ visibleWidth(scrollLabel('up', hidden, deps.iconMode)) >
205
+ outerWidth - 6)
206
+ )
207
+ return originals.renderTopBorder.call(this, width, hidden);
208
+ const status = deps.working.status(
209
+ this.workingStatusIndicator,
210
+ inputLabelWidth(outerWidth, hidden, deps.iconMode),
211
+ muted,
212
+ renderFrame,
213
+ deps.iconMode,
214
+ );
215
+ const data = active ? deps.getStatusSnapshot?.() : undefined;
216
+ const options = { mode: deps.iconMode, theme: deps.theme };
217
+ top = renderInputTop(
218
+ outerWidth,
219
+ deps.theme,
220
+ {
221
+ left: [status, data ? formatBranchSegment(data, options) : ''],
222
+ right: data ? formatCwdSegments(data, options) : [],
223
+ },
224
+ hidden,
225
+ renderFrame,
226
+ deps.iconMode,
227
+ );
228
+ return top;
229
+ },
230
+ },
231
+ renderBottomBorder: {
232
+ configurable: true,
233
+ writable: true,
234
+ value(this: EditorLike, width: number, hidden: number): string {
235
+ if (
236
+ outerWidth === undefined ||
237
+ (hidden > 0 &&
238
+ visibleWidth(scrollLabel('down', hidden, deps.iconMode)) >
239
+ outerWidth - 6)
240
+ )
241
+ return originals.renderBottomBorder.call(this, width, hidden);
242
+ const data = active ? deps.getStatusSnapshot?.() : undefined;
243
+ const options = { mode: deps.iconMode, theme: deps.theme };
244
+ bottom = renderInputBottom(
245
+ outerWidth,
246
+ deps.theme,
247
+ {
248
+ left: data ? formatModelSegments(data, options) : [],
249
+ right: data ? formatContextSegments(data, options) : [],
250
+ },
251
+ hidden,
252
+ renderFrame,
253
+ deps.iconMode,
254
+ );
255
+ return bottom;
256
+ },
257
+ },
258
+ handleMouse: {
259
+ configurable: true,
260
+ writable: true,
261
+ value(
262
+ this: EditorLike,
263
+ event: TuiMouseEvent,
264
+ ): TuiMouseEventResult | undefined {
265
+ const translated =
266
+ active && boxRendered
267
+ ? { ...event, x: event.x - 1, width: event.width - 2 }
268
+ : event;
269
+ return originals.handleMouse.call(this, translated);
270
+ },
271
+ },
272
+ borderColor: {
273
+ configurable: true,
274
+ get: () => (fallback ? originals.borderColor : muted),
275
+ set: () => {},
276
+ },
277
+ };
278
+ Object.defineProperties(editor, patches);
279
+ const decoration: EditorDecoration = {
280
+ getStatusSnapshot: () => (active ? deps.getStatusSnapshot?.() : undefined),
281
+ dispose() {
282
+ if (!active) return;
283
+ active = false;
284
+ for (const name of names) {
285
+ const current = Reflect.getOwnPropertyDescriptor(editor, name);
286
+ const patch = patches[name];
287
+ if (
288
+ current?.value !== patch.value ||
289
+ current?.get !== patch.get ||
290
+ current?.set !== patch.set
291
+ )
292
+ continue;
293
+ const descriptor = descriptors.get(name);
294
+ if (descriptor) Object.defineProperty(editor, name, descriptor);
295
+ else Reflect.deleteProperty(editor, name);
296
+ }
297
+ decoratedEditors.delete(editor);
298
+ decorations.delete(editor);
299
+ },
300
+ };
301
+ decoratedEditors.add(editor);
302
+ decorations.set(editor, decoration);
303
+ return decoration;
304
+ }