@nedleeds/pi-compact-ui 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.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/examples/compact-tools.json +14 -0
- package/extensions/compact-tools.ts +528 -0
- package/package.json +62 -0
- package/schemas/compact-tools.schema.json +62 -0
- package/scripts/check-resources.mjs +23 -0
- package/themes/github-dark-pro.json +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dong Hyeong Lee
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# pi-compact-ui
|
|
2
|
+
|
|
3
|
+
Compact, expandable rendering for pi's built-in `read`, `write`, `edit`, and `bash` tools, bundled with a polished GitHub Dark theme.
|
|
4
|
+
|
|
5
|
+
Designed for a focused, low-noise terminal workflow.
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
## Demo
|
|
10
|
+
|
|
11
|
+
**Keyboard expansion** – press `Ctrl+O` to cycle through detail levels.
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+
|
|
15
|
+
**Edit diffs** – click a finished `edit` row to reveal the diff, click again to collapse.
|
|
16
|
+
|
|
17
|
+

|
|
18
|
+
|
|
19
|
+
## Features
|
|
20
|
+
|
|
21
|
+
- Compact one-line tool calls
|
|
22
|
+
- Click or `Ctrl+O` to cycle through available detail levels
|
|
23
|
+
- Skips argument and output levels that contain no additional information
|
|
24
|
+
- Animated, configurable tool spinner
|
|
25
|
+
- Execution duration with configurable indicators
|
|
26
|
+
- Short and full output previews
|
|
27
|
+
- Clean edit diffs without duplicate JSON arguments or leading blank lines
|
|
28
|
+
- GitHub Dark theme with distinct tool, output, success, and error colors
|
|
29
|
+
- No Nerd Font requirement
|
|
30
|
+
|
|
31
|
+
## Requirements
|
|
32
|
+
|
|
33
|
+
- pi `0.85.1` or newer is recommended
|
|
34
|
+
- Node.js 20 or newer
|
|
35
|
+
- Fullscreen TUI mode is required for mouse-click expansion; `Ctrl+O` works without it
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
From npm:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pi install npm:@nedleeds/pi-compact-ui
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or directly from GitHub:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pi install git:github.com/nedleeds/pi-compact-ui
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Restart pi or run `/reload` after installation.
|
|
52
|
+
|
|
53
|
+
## Theme
|
|
54
|
+
|
|
55
|
+
The package includes the `github-dark-pro` theme. Select it from pi's settings UI or set it in `~/.pi/agent/settings.json`:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"theme": "github-dark-pro"
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Configuration
|
|
64
|
+
|
|
65
|
+
The extension works without a configuration file. Its defaults are defined in the source code.
|
|
66
|
+
|
|
67
|
+
To customize it globally, create `~/.pi/agent/compact-tools.json` using the example below. The included JSON Schema provides completion and validation in compatible editors.
|
|
68
|
+
|
|
69
|
+
For project-specific settings, create:
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
<project>/.pi/compact-tools.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Project configuration is loaded only for trusted projects. Values are merged in this order:
|
|
76
|
+
|
|
77
|
+
```text
|
|
78
|
+
built-in defaults
|
|
79
|
+
→ ~/.pi/agent/compact-tools.json
|
|
80
|
+
→ <project>/.pi/compact-tools.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Default configuration:
|
|
84
|
+
|
|
85
|
+
```json
|
|
86
|
+
{
|
|
87
|
+
"$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/schemas/compact-tools.schema.json",
|
|
88
|
+
"previewLines": 10,
|
|
89
|
+
"spinner": {
|
|
90
|
+
"frames": ["◐", "◓", "◑", "◒"],
|
|
91
|
+
"intervalMs": 120
|
|
92
|
+
},
|
|
93
|
+
"durationIndicators": [
|
|
94
|
+
{ "underMs": 1000, "icon": "⚡️" },
|
|
95
|
+
{ "underMs": 10000, "icon": "🚀" },
|
|
96
|
+
{ "underMs": 30000, "icon": "🔥" },
|
|
97
|
+
{ "icon": "⏳" }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`durationIndicators` must use ascending `underMs` values. The final entry must omit `underMs` and acts as the fallback.
|
|
103
|
+
|
|
104
|
+
## Expansion levels
|
|
105
|
+
|
|
106
|
+
Only levels with meaningful content are included. Depending on the tool call, the cycle may be:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
summary → arguments → output preview → full output → summary
|
|
110
|
+
summary → output → summary
|
|
111
|
+
summary only
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`edit` uses:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
summary → diff → summary
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Compatibility and limitations
|
|
121
|
+
|
|
122
|
+
- The package overrides pi's built-in `read`, `write`, `edit`, and `bash` definitions while preserving their execution behavior and metadata.
|
|
123
|
+
- Another extension overriding the same tool names may conflict depending on extension load order.
|
|
124
|
+
- Tools registered by other extensions are not modified.
|
|
125
|
+
- Duration is measured from the first execution render and is intended as a UI estimate.
|
|
126
|
+
- Emoji appearance depends on terminal and system font support. All default duration icons use Unicode 6.0 or earlier.
|
|
127
|
+
|
|
128
|
+
## Development
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npm install
|
|
132
|
+
npm run check
|
|
133
|
+
pi -e .
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
When testing with `pi -e .`, disable any local copy of `compact-tools.ts` to avoid duplicate tool overrides.
|
|
137
|
+
|
|
138
|
+
## Security
|
|
139
|
+
|
|
140
|
+
Pi extensions execute with full system access. Review extension source before installing third-party packages.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/schemas/compact-tools.schema.json",
|
|
3
|
+
"previewLines": 10,
|
|
4
|
+
"spinner": {
|
|
5
|
+
"frames": ["◐", "◓", "◑", "◒"],
|
|
6
|
+
"intervalMs": 120
|
|
7
|
+
},
|
|
8
|
+
"durationIndicators": [
|
|
9
|
+
{ "underMs": 1000, "icon": "⚡️" },
|
|
10
|
+
{ "underMs": 10000, "icon": "🚀" },
|
|
11
|
+
{ "underMs": 30000, "icon": "🔥" },
|
|
12
|
+
{ "icon": "⏳" }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/** Compact rendering and shared status indicators for built-in tools. */
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type {
|
|
6
|
+
AgentToolResult,
|
|
7
|
+
BashToolDetails,
|
|
8
|
+
BashToolInput,
|
|
9
|
+
ExtensionAPI,
|
|
10
|
+
Theme,
|
|
11
|
+
ToolDefinition,
|
|
12
|
+
ToolRenderResultOptions,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import {
|
|
15
|
+
CONFIG_DIR_NAME,
|
|
16
|
+
createBashToolDefinition,
|
|
17
|
+
createEditToolDefinition,
|
|
18
|
+
createReadToolDefinition,
|
|
19
|
+
createWriteToolDefinition,
|
|
20
|
+
getAgentDir,
|
|
21
|
+
} from "@earendil-works/pi-coding-agent";
|
|
22
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
23
|
+
import { Container, Text, visibleWidth } from "@earendil-works/pi-tui";
|
|
24
|
+
|
|
25
|
+
const MAX_LEVEL = 3;
|
|
26
|
+
const CONFIG_FILE = "compact-tools.json";
|
|
27
|
+
|
|
28
|
+
export interface DurationIndicatorConfig {
|
|
29
|
+
underMs?: number;
|
|
30
|
+
icon: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CompactToolsConfig {
|
|
34
|
+
previewLines: number;
|
|
35
|
+
spinner: {
|
|
36
|
+
frames: string[];
|
|
37
|
+
intervalMs: number;
|
|
38
|
+
};
|
|
39
|
+
durationIndicators: DurationIndicatorConfig[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export const DEFAULT_CONFIG: CompactToolsConfig = {
|
|
43
|
+
previewLines: 10,
|
|
44
|
+
spinner: {
|
|
45
|
+
frames: ["◐", "◓", "◑", "◒"],
|
|
46
|
+
intervalMs: 120,
|
|
47
|
+
},
|
|
48
|
+
durationIndicators: [
|
|
49
|
+
{ underMs: 1_000, icon: "⚡️" },
|
|
50
|
+
{ underMs: 10_000, icon: "🚀" },
|
|
51
|
+
{ underMs: 30_000, icon: "🔥" },
|
|
52
|
+
{ icon: "☕" },
|
|
53
|
+
],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let config = DEFAULT_CONFIG;
|
|
57
|
+
|
|
58
|
+
interface RowState {
|
|
59
|
+
level?: number;
|
|
60
|
+
availableLevels?: number[];
|
|
61
|
+
lastExpanded?: boolean;
|
|
62
|
+
frame?: number;
|
|
63
|
+
startedAt?: number;
|
|
64
|
+
endedAt?: number;
|
|
65
|
+
timer?: ReturnType<typeof setInterval>;
|
|
66
|
+
originalResultComponent?: Component;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
type ToolArgs = Record<string, unknown>;
|
|
70
|
+
type BuiltInDefinition = ToolDefinition<any, any, any>;
|
|
71
|
+
type BaseRenderContext = Parameters<NonNullable<BuiltInDefinition["renderCall"]>>[2];
|
|
72
|
+
type RenderContext<TArgs = ToolArgs> = Omit<BaseRenderContext, "args" | "state"> & {
|
|
73
|
+
args: TArgs;
|
|
74
|
+
state: RowState;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type JsonObject = Record<string, unknown>;
|
|
78
|
+
|
|
79
|
+
function isObject(value: unknown): value is JsonObject {
|
|
80
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function warnConfig(path: string, message: string): void {
|
|
84
|
+
console.error(`[compact-tools] ${path}: ${message}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isIntegerInRange(value: unknown, minimum: number, maximum: number): value is number {
|
|
88
|
+
return typeof value === "number" && Number.isInteger(value) && value >= minimum && value <= maximum;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isNonEmptyStringArray(value: unknown): value is string[] {
|
|
92
|
+
return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function parseDurationIndicators(value: unknown): DurationIndicatorConfig[] | undefined {
|
|
96
|
+
if (!Array.isArray(value) || value.length === 0) return undefined;
|
|
97
|
+
const rules: DurationIndicatorConfig[] = [];
|
|
98
|
+
let previousLimit = 0;
|
|
99
|
+
for (const [index, item] of value.entries()) {
|
|
100
|
+
if (!isObject(item) || typeof item.icon !== "string" || item.icon.length === 0) return undefined;
|
|
101
|
+
const isLast = index === value.length - 1;
|
|
102
|
+
if (isLast && item.underMs === undefined) rules.push({ icon: item.icon });
|
|
103
|
+
else if (isIntegerInRange(item.underMs, previousLimit + 1, Number.MAX_SAFE_INTEGER)) {
|
|
104
|
+
previousLimit = item.underMs;
|
|
105
|
+
rules.push({ underMs: item.underMs, icon: item.icon });
|
|
106
|
+
} else return undefined;
|
|
107
|
+
}
|
|
108
|
+
return rules.at(-1)?.underMs === undefined ? rules : undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function mergeConfig(base: CompactToolsConfig, value: unknown, path: string): CompactToolsConfig {
|
|
112
|
+
if (value === undefined) return base;
|
|
113
|
+
if (!isObject(value)) {
|
|
114
|
+
warnConfig(path, "expected a JSON object; using previous values");
|
|
115
|
+
return base;
|
|
116
|
+
}
|
|
117
|
+
const previewValue = value.previewLines;
|
|
118
|
+
const validPreviewLines = isIntegerInRange(previewValue, 1, 1_000);
|
|
119
|
+
const previewLines = validPreviewLines ? previewValue : base.previewLines;
|
|
120
|
+
if (previewValue !== undefined && !validPreviewLines) {
|
|
121
|
+
warnConfig(path, "previewLines must be an integer from 1 to 1000; using previous value");
|
|
122
|
+
}
|
|
123
|
+
const spinner = parseSpinnerConfig(base.spinner, value.spinner, path);
|
|
124
|
+
const durationIndicators = parseDurationIndicators(value.durationIndicators) ?? base.durationIndicators;
|
|
125
|
+
if (value.durationIndicators !== undefined && durationIndicators === base.durationIndicators) {
|
|
126
|
+
warnConfig(path, "invalid durationIndicators; using previous values");
|
|
127
|
+
}
|
|
128
|
+
return { previewLines, spinner, durationIndicators };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseSpinnerConfig(
|
|
132
|
+
base: CompactToolsConfig["spinner"],
|
|
133
|
+
value: unknown,
|
|
134
|
+
path: string,
|
|
135
|
+
): CompactToolsConfig["spinner"] {
|
|
136
|
+
if (value === undefined) return base;
|
|
137
|
+
if (!isObject(value)) {
|
|
138
|
+
warnConfig(path, "spinner must be an object; using previous values");
|
|
139
|
+
return base;
|
|
140
|
+
}
|
|
141
|
+
const framesValue = value.frames;
|
|
142
|
+
const intervalValue = value.intervalMs;
|
|
143
|
+
const validFrames = isNonEmptyStringArray(framesValue);
|
|
144
|
+
const validInterval = isIntegerInRange(intervalValue, 40, 5_000);
|
|
145
|
+
if (framesValue !== undefined && !validFrames) warnConfig(path, "spinner.frames must be a non-empty string array");
|
|
146
|
+
if (intervalValue !== undefined && !validInterval) warnConfig(path, "spinner.intervalMs must be 40–5000");
|
|
147
|
+
return {
|
|
148
|
+
frames: validFrames ? framesValue : base.frames,
|
|
149
|
+
intervalMs: validInterval ? intervalValue : base.intervalMs,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readConfig(path: string): unknown {
|
|
154
|
+
if (!existsSync(path)) return undefined;
|
|
155
|
+
try {
|
|
156
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
157
|
+
} catch (error) {
|
|
158
|
+
warnConfig(path, error instanceof Error ? error.message : String(error));
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function loadConfig(cwd?: string, projectTrusted = false): CompactToolsConfig {
|
|
164
|
+
const globalPath = join(getAgentDir(), CONFIG_FILE);
|
|
165
|
+
let loaded = mergeConfig(DEFAULT_CONFIG, readConfig(globalPath), globalPath);
|
|
166
|
+
if (!cwd || !projectTrusted) return loaded;
|
|
167
|
+
const projectPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE);
|
|
168
|
+
loaded = mergeConfig(loaded, readConfig(projectPath), projectPath);
|
|
169
|
+
return loaded;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const SPINNING_ROWS_KEY = Symbol.for("pi.compact-tools.spinning-rows");
|
|
173
|
+
const globalState = globalThis as typeof globalThis & { [SPINNING_ROWS_KEY]?: Set<RowState> };
|
|
174
|
+
for (const staleState of globalState[SPINNING_ROWS_KEY] ?? []) {
|
|
175
|
+
if (staleState.timer) clearInterval(staleState.timer);
|
|
176
|
+
}
|
|
177
|
+
const spinningRows = new Set<RowState>();
|
|
178
|
+
globalState[SPINNING_ROWS_KEY] = spinningRows;
|
|
179
|
+
|
|
180
|
+
function advanceLevel(state: RowState, expanded: boolean): number {
|
|
181
|
+
state.level ??= 0;
|
|
182
|
+
if (state.lastExpanded === undefined) {
|
|
183
|
+
state.lastExpanded = expanded;
|
|
184
|
+
return state.level;
|
|
185
|
+
}
|
|
186
|
+
if (state.lastExpanded !== expanded) {
|
|
187
|
+
state.lastExpanded = expanded;
|
|
188
|
+
const levels = state.availableLevels ?? [0, 1];
|
|
189
|
+
const index = levels.indexOf(state.level);
|
|
190
|
+
state.level = levels[(index + 1) % levels.length] ?? 0;
|
|
191
|
+
}
|
|
192
|
+
return state.level;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function setAvailableLevels(state: RowState, levels: number[]): void {
|
|
196
|
+
state.availableLevels = levels;
|
|
197
|
+
if (!levels.includes(state.level ?? 0)) state.level = 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getOutputLevels(output: string): number[] {
|
|
201
|
+
const normalized = output.trimEnd();
|
|
202
|
+
if (!normalized) return [];
|
|
203
|
+
return normalized.split("\n").length > config.previewLines ? [2, 3] : [2];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function stopSpinner(state: RowState): void {
|
|
207
|
+
if (state.timer) clearInterval(state.timer);
|
|
208
|
+
state.timer = undefined;
|
|
209
|
+
spinningRows.delete(state);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function syncSpinner(state: RowState, running: boolean, invalidate: () => void): void {
|
|
213
|
+
if (!running) {
|
|
214
|
+
stopSpinner(state);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (state.timer) return;
|
|
218
|
+
|
|
219
|
+
state.frame ??= 0;
|
|
220
|
+
state.timer = setInterval(() => {
|
|
221
|
+
state.frame = ((state.frame ?? 0) + 1) % config.spinner.frames.length;
|
|
222
|
+
try {
|
|
223
|
+
invalidate();
|
|
224
|
+
} catch {
|
|
225
|
+
stopSpinner(state);
|
|
226
|
+
}
|
|
227
|
+
}, config.spinner.intervalMs);
|
|
228
|
+
state.timer.unref?.();
|
|
229
|
+
spinningRows.add(state);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function syncTiming(state: RowState, executionStarted: boolean, running: boolean): void {
|
|
233
|
+
if ((executionStarted || running) && state.startedAt === undefined) state.startedAt = Date.now();
|
|
234
|
+
if (!running && state.startedAt !== undefined && state.endedAt === undefined) {
|
|
235
|
+
state.endedAt = Date.now();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function syncRow(ctx: RenderContext, running = ctx.executionStarted && ctx.isPartial): RowState {
|
|
240
|
+
const state = ctx.state;
|
|
241
|
+
syncTiming(state, ctx.executionStarted, running);
|
|
242
|
+
syncSpinner(state, running, ctx.invalidate);
|
|
243
|
+
return state;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function resetUiState(): void {
|
|
247
|
+
for (const state of [...spinningRows]) stopSpinner(state);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function renderIndicator(
|
|
251
|
+
theme: Theme,
|
|
252
|
+
state: RowState,
|
|
253
|
+
running: boolean,
|
|
254
|
+
isError: boolean,
|
|
255
|
+
pending: boolean,
|
|
256
|
+
): string {
|
|
257
|
+
if (isError) return theme.fg("error", "⊗");
|
|
258
|
+
if (pending) return theme.fg("muted", "·");
|
|
259
|
+
if (!running) return theme.fg("success", "●");
|
|
260
|
+
const frame = config.spinner.frames[state.frame ?? 0] ?? config.spinner.frames[0] ?? "◐";
|
|
261
|
+
return theme.fg("muted", frame);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function formatDuration(state: RowState): string | undefined {
|
|
265
|
+
if (state.startedAt === undefined) return undefined;
|
|
266
|
+
const elapsedMs = (state.endedAt ?? Date.now()) - state.startedAt;
|
|
267
|
+
return `${(elapsedMs / 1000).toFixed(1)}s`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function durationIndicator(state: RowState): string | undefined {
|
|
271
|
+
if (state.startedAt === undefined || state.endedAt === undefined) return undefined;
|
|
272
|
+
const elapsedMs = state.endedAt - state.startedAt;
|
|
273
|
+
return config.durationIndicators.find((rule) => rule.underMs === undefined || elapsedMs < rule.underMs)?.icon;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function renderControls(theme: Theme, state: RowState, running: boolean, isError: boolean): Text {
|
|
277
|
+
const duration = formatDuration(state);
|
|
278
|
+
const levels = state.availableLevels ?? [0, 1];
|
|
279
|
+
const expandable = levels.length > 1;
|
|
280
|
+
const atLastLevel = state.level === levels.at(-1);
|
|
281
|
+
const action = expandable
|
|
282
|
+
? `${theme.italic("ctrl+o")} ${atLastLevel ? "to collapse" : "for more"}`
|
|
283
|
+
: undefined;
|
|
284
|
+
const indicator = !running && !isError ? durationIndicator(state) : undefined;
|
|
285
|
+
const status = running
|
|
286
|
+
? duration
|
|
287
|
+
: `${indicator ? `${indicator} ` : ""}${isError ? "Failed" : "Done"}${duration ? ` in ${duration}` : ""}`;
|
|
288
|
+
const details = [status, action].filter(Boolean).join(", ");
|
|
289
|
+
return new Text(` ${theme.fg("dim", details)}`, 0, 0);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function firstLine(value: string, maxLength = 100): string {
|
|
293
|
+
const line = value.split("\n")[0] ?? "";
|
|
294
|
+
return line.length > maxLength ? `${line.slice(0, maxLength - 1)}…` : line;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getTextResult(result: AgentToolResult<unknown>): string {
|
|
298
|
+
return result.content
|
|
299
|
+
.filter((item) => item.type === "text")
|
|
300
|
+
.map((item) => item.text)
|
|
301
|
+
.join("\n")
|
|
302
|
+
.trimEnd();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function withoutLeadingBlankLines(component: Component): Component {
|
|
306
|
+
return {
|
|
307
|
+
render(width: number) {
|
|
308
|
+
const lines = component.render(width);
|
|
309
|
+
const firstContentLine = lines.findIndex((line) => visibleWidth(line.trim()) > 0);
|
|
310
|
+
return firstContentLine < 0 ? [] : lines.slice(firstContentLine);
|
|
311
|
+
},
|
|
312
|
+
invalidate() {
|
|
313
|
+
component.invalidate?.();
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function styleToolOutput(text: string, theme: Theme, isError: boolean): string {
|
|
319
|
+
const color = isError ? "error" : "toolOutput";
|
|
320
|
+
return text
|
|
321
|
+
.split(/(⚡️?)/u)
|
|
322
|
+
.map((part) => theme.fg(!isError && part.startsWith("⚡") ? "warning" : color, part))
|
|
323
|
+
.join("");
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function renderPreview(output: string, level: number, theme: Theme, isError: boolean): Text | undefined {
|
|
327
|
+
const normalized = output.trimEnd();
|
|
328
|
+
if (!normalized) return undefined;
|
|
329
|
+
|
|
330
|
+
const lines = normalized.split("\n");
|
|
331
|
+
const shown = level >= MAX_LEVEL ? lines : lines.slice(0, config.previewLines);
|
|
332
|
+
let text = shown.map((line) => ` ${line}`).join("\n");
|
|
333
|
+
if (shown.length < lines.length) text += `\n … ${lines.length - shown.length} more lines`;
|
|
334
|
+
return new Text(styleToolOutput(text, theme, isError), 0, 0);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function getPathArg(args: ToolArgs): string {
|
|
338
|
+
const value = args.path ?? args.file_path;
|
|
339
|
+
return typeof value === "string" ? firstLine(value) : "";
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getFileArgumentDetails(name: string, args: ToolArgs): ToolArgs {
|
|
343
|
+
if (name === "edit") return {};
|
|
344
|
+
const omitted = new Set(["path", "file_path"]);
|
|
345
|
+
if (name === "write") omitted.add("content");
|
|
346
|
+
return Object.fromEntries(Object.entries(args).filter(([key, value]) => !omitted.has(key) && value !== undefined));
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function renderArguments(args: ToolArgs, theme: Theme): Text {
|
|
350
|
+
const json = JSON.stringify(args, null, 2) ?? "{}";
|
|
351
|
+
const text = json
|
|
352
|
+
.split("\n")
|
|
353
|
+
.map((line) => ` ${line}`)
|
|
354
|
+
.join("\n");
|
|
355
|
+
return new Text(theme.fg("toolOutput", text), 0, 0);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function callOriginalEditResult(
|
|
359
|
+
definition: BuiltInDefinition,
|
|
360
|
+
result: AgentToolResult<unknown>,
|
|
361
|
+
options: ToolRenderResultOptions,
|
|
362
|
+
theme: Theme,
|
|
363
|
+
ctx: RenderContext,
|
|
364
|
+
state: RowState,
|
|
365
|
+
level: number,
|
|
366
|
+
): void {
|
|
367
|
+
if (definition.name !== "edit" || !definition.renderResult) return;
|
|
368
|
+
state.originalResultComponent = definition.renderResult(
|
|
369
|
+
result,
|
|
370
|
+
{ ...options, expanded: level >= MAX_LEVEL },
|
|
371
|
+
theme,
|
|
372
|
+
{ ...ctx, state: {}, lastComponent: state.originalResultComponent },
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getFileOutput(name: string, args: ToolArgs, result: AgentToolResult<unknown>, isError: boolean): string {
|
|
377
|
+
if (isError || name === "read") return getTextResult(result);
|
|
378
|
+
if (name === "write") return String(args.content ?? "");
|
|
379
|
+
return "";
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function renderFileCall(
|
|
383
|
+
definition: BuiltInDefinition,
|
|
384
|
+
args: ToolArgs,
|
|
385
|
+
theme: Theme,
|
|
386
|
+
ctx: RenderContext,
|
|
387
|
+
): Container {
|
|
388
|
+
const running = ctx.executionStarted && ctx.isPartial;
|
|
389
|
+
const state = syncRow(ctx, running);
|
|
390
|
+
const level = advanceLevel(state, ctx.expanded);
|
|
391
|
+
const path = getPathArg(args);
|
|
392
|
+
const argumentDetails = getFileArgumentDetails(definition.name, args);
|
|
393
|
+
let text = `${renderIndicator(theme, state, running, ctx.isError, ctx.isPartial && !ctx.executionStarted)} `;
|
|
394
|
+
text += theme.fg("toolTitle", theme.bold(definition.name));
|
|
395
|
+
if (path) text += ` ${theme.fg("toolOutput", path)}`;
|
|
396
|
+
|
|
397
|
+
const container = new Container();
|
|
398
|
+
container.addChild(new Text(text, 1, 0));
|
|
399
|
+
if (level >= 1 && Object.keys(argumentDetails).length > 0) {
|
|
400
|
+
container.addChild(renderArguments(argumentDetails, theme));
|
|
401
|
+
}
|
|
402
|
+
return container;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function addFileResultPreview(
|
|
406
|
+
container: Container,
|
|
407
|
+
definition: BuiltInDefinition,
|
|
408
|
+
state: RowState,
|
|
409
|
+
output: string,
|
|
410
|
+
level: number,
|
|
411
|
+
theme: Theme,
|
|
412
|
+
isError: boolean,
|
|
413
|
+
): void {
|
|
414
|
+
if (level < 2) return;
|
|
415
|
+
if (definition.name === "edit") {
|
|
416
|
+
if (state.originalResultComponent) {
|
|
417
|
+
container.addChild(withoutLeadingBlankLines(state.originalResultComponent));
|
|
418
|
+
}
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const preview = renderPreview(output, level, theme, isError);
|
|
422
|
+
if (preview) container.addChild(preview);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function renderFileResult(
|
|
426
|
+
definition: BuiltInDefinition,
|
|
427
|
+
result: AgentToolResult<unknown>,
|
|
428
|
+
options: ToolRenderResultOptions,
|
|
429
|
+
theme: Theme,
|
|
430
|
+
ctx: RenderContext,
|
|
431
|
+
): Container {
|
|
432
|
+
const state = syncRow(ctx, options.isPartial);
|
|
433
|
+
const output = getFileOutput(definition.name, ctx.args, result, ctx.isError);
|
|
434
|
+
const hasArguments = Object.keys(getFileArgumentDetails(definition.name, ctx.args)).length > 0;
|
|
435
|
+
const levels = definition.name === "edit" ? [0, 2] : [0, ...(hasArguments ? [1] : []), ...getOutputLevels(output)];
|
|
436
|
+
setAvailableLevels(state, levels);
|
|
437
|
+
|
|
438
|
+
const level = state.level ?? 0;
|
|
439
|
+
callOriginalEditResult(definition, result, options, theme, ctx, state, level);
|
|
440
|
+
const container = new Container();
|
|
441
|
+
addFileResultPreview(container, definition, state, output, level, theme, ctx.isError);
|
|
442
|
+
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError));
|
|
443
|
+
return container;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function createFileCallRenderer(definition: BuiltInDefinition) {
|
|
447
|
+
return (args: ToolArgs, theme: Theme, ctx: RenderContext) => renderFileCall(definition, args, theme, ctx);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function createFileResultRenderer(definition: BuiltInDefinition) {
|
|
451
|
+
return (result: AgentToolResult<unknown>, options: ToolRenderResultOptions, theme: Theme, ctx: RenderContext) =>
|
|
452
|
+
renderFileResult(definition, result, options, theme, ctx);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function registerFileTool(pi: ExtensionAPI, definition: BuiltInDefinition): void {
|
|
456
|
+
const tool = {
|
|
457
|
+
...definition,
|
|
458
|
+
renderShell: "self" as const,
|
|
459
|
+
renderCall: createFileCallRenderer(definition),
|
|
460
|
+
renderResult: createFileResultRenderer(definition),
|
|
461
|
+
};
|
|
462
|
+
// Built-in file schemas are intentionally erased at this shared renderer boundary.
|
|
463
|
+
pi.registerTool(tool as ToolDefinition<any, any, RowState>);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function renderBashCall(args: BashToolInput, theme: Theme, ctx: RenderContext<BashToolInput>): Text {
|
|
467
|
+
const running = ctx.executionStarted && ctx.isPartial;
|
|
468
|
+
const state = syncRow(ctx, running);
|
|
469
|
+
const level = advanceLevel(state, ctx.expanded);
|
|
470
|
+
const command = args.command ?? "";
|
|
471
|
+
const displayedCommand = (level >= 1 ? command : firstLine(command)) || "…";
|
|
472
|
+
let text = `${renderIndicator(theme, state, running, ctx.isError, ctx.isPartial && !ctx.executionStarted)} `;
|
|
473
|
+
text += `${theme.fg("toolTitle", theme.bold("bash"))} ${theme.fg("toolOutput", displayedCommand)}`;
|
|
474
|
+
if (level >= 1 && args.timeout) text += theme.fg("dim", ` (timeout: ${args.timeout}s)`);
|
|
475
|
+
return new Text(text, 1, 0);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function renderBashResult(
|
|
479
|
+
result: AgentToolResult<BashToolDetails | undefined>,
|
|
480
|
+
options: ToolRenderResultOptions,
|
|
481
|
+
theme: Theme,
|
|
482
|
+
ctx: RenderContext<BashToolInput>,
|
|
483
|
+
): Component {
|
|
484
|
+
const state = syncRow(ctx, options.isPartial);
|
|
485
|
+
const output = getTextResult(result);
|
|
486
|
+
const hasDetailedCall = (ctx.args.command ?? "").includes("\n") || ctx.args.timeout !== undefined;
|
|
487
|
+
setAvailableLevels(state, [0, ...(hasDetailedCall ? [1] : []), ...getOutputLevels(output)]);
|
|
488
|
+
const level = state.level ?? 0;
|
|
489
|
+
if (level < 2) return renderControls(theme, state, options.isPartial, ctx.isError);
|
|
490
|
+
|
|
491
|
+
const container = new Container();
|
|
492
|
+
const preview = renderPreview(output, level, theme, ctx.isError);
|
|
493
|
+
if (preview) container.addChild(preview);
|
|
494
|
+
else container.addChild(new Text(theme.fg("dim", options.isPartial ? " …" : " (no output)"), 0, 0));
|
|
495
|
+
container.addChild(renderControls(theme, state, options.isPartial, ctx.isError));
|
|
496
|
+
return container;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function registerBashTool(pi: ExtensionAPI, cwd: string): void {
|
|
500
|
+
const original = createBashToolDefinition(cwd);
|
|
501
|
+
pi.registerTool<typeof original.parameters, BashToolDetails | undefined, RowState>({
|
|
502
|
+
...original,
|
|
503
|
+
renderShell: "self",
|
|
504
|
+
renderCall: renderBashCall,
|
|
505
|
+
renderResult: renderBashResult,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function registerBuiltInTools(pi: ExtensionAPI, cwd: string): void {
|
|
510
|
+
registerFileTool(pi, createReadToolDefinition(cwd));
|
|
511
|
+
registerFileTool(pi, createEditToolDefinition(cwd));
|
|
512
|
+
registerFileTool(pi, createWriteToolDefinition(cwd));
|
|
513
|
+
registerBashTool(pi, cwd);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function applyConfig(pi: ExtensionAPI, cwd: string, projectTrusted: boolean): void {
|
|
517
|
+
resetUiState();
|
|
518
|
+
config = loadConfig(cwd, projectTrusted);
|
|
519
|
+
registerBuiltInTools(pi, cwd);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export default function (pi: ExtensionAPI): void {
|
|
523
|
+
config = loadConfig();
|
|
524
|
+
registerBuiltInTools(pi, process.cwd());
|
|
525
|
+
pi.on("session_start", (_event, ctx) => applyConfig(pi, ctx.cwd, ctx.isProjectTrusted()));
|
|
526
|
+
pi.on("resources_discover", (_event, ctx) => applyConfig(pi, ctx.cwd, ctx.isProjectTrusted()));
|
|
527
|
+
pi.on("session_shutdown", resetUiState);
|
|
528
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nedleeds/pi-compact-ui",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Compact, expandable UI for pi's built-in tools, bundled with a polished GitHub Dark theme.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Dong Hyeong Lee",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/nedleeds/pi-compact-ui.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/nedleeds/pi-compact-ui#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/nedleeds/pi-compact-ui/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"pi-package",
|
|
18
|
+
"pi-coding-agent",
|
|
19
|
+
"pi-extension",
|
|
20
|
+
"terminal-ui",
|
|
21
|
+
"theme"
|
|
22
|
+
],
|
|
23
|
+
"files": [
|
|
24
|
+
"extensions",
|
|
25
|
+
"themes",
|
|
26
|
+
"examples",
|
|
27
|
+
"scripts",
|
|
28
|
+
"schemas",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"check": "tsc --noEmit && npm run check:resources",
|
|
34
|
+
"check:resources": "node scripts/check-resources.mjs",
|
|
35
|
+
"prepublishOnly": "npm run check"
|
|
36
|
+
},
|
|
37
|
+
"pi": {
|
|
38
|
+
"extensions": [
|
|
39
|
+
"./extensions/compact-tools.ts"
|
|
40
|
+
],
|
|
41
|
+
"themes": [
|
|
42
|
+
"./themes/github-dark-pro.json"
|
|
43
|
+
],
|
|
44
|
+
"image": "https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/assets/spinner-status.gif"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
48
|
+
"@earendil-works/pi-tui": "*"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
52
|
+
"@earendil-works/pi-tui": "0.85.1",
|
|
53
|
+
"@types/node": "^24.0.0",
|
|
54
|
+
"typescript": "^5.9.0"
|
|
55
|
+
},
|
|
56
|
+
"engines": {
|
|
57
|
+
"node": ">=20"
|
|
58
|
+
},
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://raw.githubusercontent.com/nedleeds/pi-compact-ui/main/schemas/compact-tools.schema.json",
|
|
4
|
+
"title": "pi-compact-ui configuration",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"additionalProperties": false,
|
|
7
|
+
"properties": {
|
|
8
|
+
"$schema": {
|
|
9
|
+
"type": "string"
|
|
10
|
+
},
|
|
11
|
+
"previewLines": {
|
|
12
|
+
"description": "Number of output lines shown before the full-output expansion level.",
|
|
13
|
+
"type": "integer",
|
|
14
|
+
"minimum": 1,
|
|
15
|
+
"maximum": 1000,
|
|
16
|
+
"default": 10
|
|
17
|
+
},
|
|
18
|
+
"spinner": {
|
|
19
|
+
"type": "object",
|
|
20
|
+
"additionalProperties": false,
|
|
21
|
+
"properties": {
|
|
22
|
+
"frames": {
|
|
23
|
+
"description": "Animation frames. Use terminal-safe single-width characters for consistent alignment.",
|
|
24
|
+
"type": "array",
|
|
25
|
+
"minItems": 1,
|
|
26
|
+
"items": {
|
|
27
|
+
"type": "string",
|
|
28
|
+
"minLength": 1
|
|
29
|
+
},
|
|
30
|
+
"default": ["◐", "◓", "◑", "◒"]
|
|
31
|
+
},
|
|
32
|
+
"intervalMs": {
|
|
33
|
+
"description": "Milliseconds between spinner frames.",
|
|
34
|
+
"type": "integer",
|
|
35
|
+
"minimum": 40,
|
|
36
|
+
"maximum": 5000,
|
|
37
|
+
"default": 120
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"durationIndicators": {
|
|
42
|
+
"description": "Ordered duration thresholds. The final item must omit underMs and acts as the fallback.",
|
|
43
|
+
"type": "array",
|
|
44
|
+
"minItems": 1,
|
|
45
|
+
"items": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"additionalProperties": false,
|
|
48
|
+
"required": ["icon"],
|
|
49
|
+
"properties": {
|
|
50
|
+
"underMs": {
|
|
51
|
+
"type": "integer",
|
|
52
|
+
"minimum": 1
|
|
53
|
+
},
|
|
54
|
+
"icon": {
|
|
55
|
+
"type": "string",
|
|
56
|
+
"minLength": 1
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
|
|
3
|
+
const resources = [
|
|
4
|
+
"themes/github-dark-pro.json",
|
|
5
|
+
"examples/compact-tools.json",
|
|
6
|
+
"schemas/compact-tools.schema.json",
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
for (const path of resources) {
|
|
10
|
+
const value = JSON.parse(await readFile(new URL(`../${path}`, import.meta.url), "utf8"));
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
12
|
+
throw new Error(`${path} must contain a JSON object`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const theme = JSON.parse(
|
|
17
|
+
await readFile(new URL("../themes/github-dark-pro.json", import.meta.url), "utf8"),
|
|
18
|
+
);
|
|
19
|
+
if (theme.name !== "github-dark-pro" || !theme.colors || typeof theme.colors !== "object") {
|
|
20
|
+
throw new Error("themes/github-dark-pro.json is not a valid pi theme document");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log("Resource checks passed");
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
3
|
+
"name": "github-dark-pro",
|
|
4
|
+
"vars": {
|
|
5
|
+
"surfaceMuted": "#21262D",
|
|
6
|
+
"border": "#30363D",
|
|
7
|
+
"borderSubtle": "#21262D",
|
|
8
|
+
"text": "#C9D1D9",
|
|
9
|
+
"textBright": "#E6EDF3",
|
|
10
|
+
"muted": "#8B949E",
|
|
11
|
+
"dim": "#6E7681",
|
|
12
|
+
"blue": "#58A6FF",
|
|
13
|
+
"blueSoft": "#79C0FF",
|
|
14
|
+
"green": "#3FB950",
|
|
15
|
+
"red": "#F85149",
|
|
16
|
+
"yellow": "#E0A052",
|
|
17
|
+
"orange": "#FFA657",
|
|
18
|
+
"purple": "#D2A8FF",
|
|
19
|
+
"toolAccent": "#95A7FF",
|
|
20
|
+
"toolName": "#8B85D6",
|
|
21
|
+
"toolText": "#A6AFBA",
|
|
22
|
+
"pink": "#FF7B72"
|
|
23
|
+
},
|
|
24
|
+
"colors": {
|
|
25
|
+
"accent": "toolAccent",
|
|
26
|
+
"border": "border",
|
|
27
|
+
"borderAccent": "dim",
|
|
28
|
+
"borderMuted": "borderSubtle",
|
|
29
|
+
"success": "green",
|
|
30
|
+
"error": "red",
|
|
31
|
+
"warning": "yellow",
|
|
32
|
+
"muted": "muted",
|
|
33
|
+
"dim": "dim",
|
|
34
|
+
"text": "text",
|
|
35
|
+
"thinkingText": "muted",
|
|
36
|
+
|
|
37
|
+
"selectedBg": "surfaceMuted",
|
|
38
|
+
"searchMatchBg": "#3B2E10",
|
|
39
|
+
"searchMatchText": "textBright",
|
|
40
|
+
"userMessageBg": "#0E1014",
|
|
41
|
+
"userMessageText": "textBright",
|
|
42
|
+
"customMessageBg": "",
|
|
43
|
+
"customMessageText": "text",
|
|
44
|
+
"customMessageLabel": "muted",
|
|
45
|
+
"toolPendingBg": "",
|
|
46
|
+
"toolSuccessBg": "",
|
|
47
|
+
"toolErrorBg": "#211414",
|
|
48
|
+
"toolTitle": "toolName",
|
|
49
|
+
"toolOutput": "toolText",
|
|
50
|
+
"scrollbarTrack": "borderSubtle",
|
|
51
|
+
"scrollbarThumb": "border",
|
|
52
|
+
|
|
53
|
+
"mdHeading": "textBright",
|
|
54
|
+
"mdLink": "blue",
|
|
55
|
+
"mdLinkUrl": "dim",
|
|
56
|
+
"mdCode": "blueSoft",
|
|
57
|
+
"mdCodeBlock": "text",
|
|
58
|
+
"mdCodeBlockBorder": "#484F58",
|
|
59
|
+
"mdQuote": "muted",
|
|
60
|
+
"mdQuoteBorder": "border",
|
|
61
|
+
"mdHr": "borderSubtle",
|
|
62
|
+
"mdListBullet": "muted",
|
|
63
|
+
|
|
64
|
+
"toolDiffAdded": "#45B85A",
|
|
65
|
+
"toolDiffRemoved": "#E45D57",
|
|
66
|
+
"toolDiffContext": "dim",
|
|
67
|
+
|
|
68
|
+
"syntaxComment": "dim",
|
|
69
|
+
"syntaxKeyword": "pink",
|
|
70
|
+
"syntaxFunction": "purple",
|
|
71
|
+
"syntaxVariable": "text",
|
|
72
|
+
"syntaxString": "blueSoft",
|
|
73
|
+
"syntaxNumber": "orange",
|
|
74
|
+
"syntaxType": "orange",
|
|
75
|
+
"syntaxOperator": "muted",
|
|
76
|
+
"syntaxPunctuation": "muted",
|
|
77
|
+
|
|
78
|
+
"thinkingOff": "borderSubtle",
|
|
79
|
+
"thinkingMinimal": "border",
|
|
80
|
+
"thinkingLow": "dim",
|
|
81
|
+
"thinkingMedium": "muted",
|
|
82
|
+
"thinkingHigh": "blue",
|
|
83
|
+
"thinkingXhigh": "blueSoft",
|
|
84
|
+
"thinkingMax": "blueSoft",
|
|
85
|
+
"bashMode": "green"
|
|
86
|
+
},
|
|
87
|
+
"export": {
|
|
88
|
+
"pageBg": "#0D1117",
|
|
89
|
+
"cardBg": "#161B22",
|
|
90
|
+
"infoBg": "#1C2128"
|
|
91
|
+
}
|
|
92
|
+
}
|