pi-compact-tools 0.6.3 → 0.7.7

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 CHANGED
@@ -1,183 +1,151 @@
1
- # pi-compact-tools
2
-
3
- Compact, expandable rendering for Pi's built-in tools, with a polished GitHub Dark theme.
4
-
5
- > Formerly `@nedleeds/pi-compact-ui`. New installations should use `pi-compact-tools`.
6
-
7
- ## Demo
8
-
9
- The recordings below use the optional [Nerd Font](#nerd-font-spinner-preset) spinner preset. **The default configuration does not require a Nerd Font**.
10
-
11
- ### Compact tool workflow
12
-
13
- Active, successful, and failed tool calls stay compact while preserving useful status and timing information.
14
-
15
- ![Compact tool workflow with Nerd Font](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/nerd-font-workflow-optimized.gif)
16
-
17
- ### Spinner and duration display
18
-
19
- The spinner animates during execution, then returns to a green success circle or red failure circle. Elapsed time remains visible in the neutral control color.
20
-
21
- ![Spinner and elapsed-time display](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/spinner-duration-optimized.gif)
22
-
23
- ### Initially visible edit diffs
24
-
25
- Completed `edit` rows show their full diff initially by default. The per-tool `auto_compact` setting controls which tools start with their result hidden without disabling click or `Ctrl+O` interaction.
26
-
27
- ![Edit diff](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/edit-diff.gif)
28
-
29
- ## Features
30
-
31
- - Compact rendering for `read`, `write`, `edit`, `bash`, `powershell`, `grep`, `find`, and `ls`
32
- - Animated configurable spinner in fullscreen TUI mode
33
- - Four-step `Ctrl+T` thinking cycle when provider detail is available: summary, detail, summary, then hidden
34
- - Millisecond-precision duration and completed output line counts with neutral status text
35
- - Per-tool initial compaction and global-toggle participation with `auto_compact: true` or `false`
36
- - Primary invocation targets such as paths, patterns, and shell commands stay visible
37
- - Click toggles one result between hidden and fully visible; `Ctrl+O` toggles all participating rows
38
- - Full output on expansion and initially visible edit diffs by default
39
- - Width-cached expanded results and edit-diff processing for responsive fullscreen scrolling
40
- - Dark `│` and `└─` visual grouping
41
- - Distinct cool-blue thinking text
42
- - Windows, Unix, and classic Mac line-ending support
43
- - No Nerd Font requirement
44
-
45
- ## Install
46
-
47
- ```bash
48
- pi install npm:pi-compact-tools
49
- ```
50
-
51
- Or install from GitHub:
52
-
53
- ```bash
54
- pi install git:github.com/nedleeds/pi-compact-tools
55
- ```
56
-
57
- Restart Pi or run `/reload`.
58
-
59
- Migrating from the previous package name:
60
-
61
- ```bash
62
- pi remove npm:@nedleeds/pi-compact-ui
63
- pi install npm:pi-compact-tools
64
- ```
65
-
66
- ## Theme
67
-
68
- Select `github-dark-pro` in Pi's settings, or add it to `~/.pi/agent/settings.json`:
69
-
70
- ```json
71
- {
72
- "theme": "github-dark-pro"
73
- }
74
- ```
75
-
76
- ## Configuration
77
-
78
- Configuration is optional. Create `~/.pi/agent/compact-tools.json` for global settings or `<project>/.pi/compact-tools.json` for a trusted project.
79
-
80
- Default configuration:
81
-
82
- ```json
83
- {
84
- "$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/schemas/compact-tools.schema.json",
85
- "tools": ["read", "write", "edit", "bash"],
86
- "auto_compact": {
87
- "read": true,
88
- "write": true,
89
- "edit": false,
90
- "bash": true
91
- },
92
- "spinner": {
93
- "frames": ["◐", "◓", "◑", "◒"],
94
- "intervalMs": 120
95
- }
96
- }
97
- ```
98
-
99
- Successful call titles use a green `●`, while failed call titles use a red `●`. `Done`, `Failed`, and elapsed time use the same neutral control color. Fast tools finish immediately; no artificial spinner delay is added.
100
-
101
- For a minimal blinking spinner, copy [`examples/compact-tools-2.json`](examples/compact-tools-2.json). Its frames use `● ● (blank) ● ●` at 120 ms intervals. The renderer's built-in frame dimming turns this into a fade-out/fade-in animation. Rename it to `compact-tools.json` or copy its contents to the active configuration path.
102
-
103
- `auto_compact` controls each tool's initial state and whether it participates in the global `Ctrl+O` toggle:
104
-
105
- - `true` starts with the result hidden and participates in `Ctrl+O`.
106
- - `false` starts with the full result visible and is unaffected by `Ctrl+O`.
107
- - Clicking any compact-rendered row still toggles that row's result.
108
-
109
- Unspecified entries inherit the previous configuration layer. By default, `read`, `write`, and `bash` start compact and participate in the global toggle, while `edit` starts expanded and remains unaffected so code diffs stay visible.
110
-
111
- To enable all Unix-compatible tools:
112
-
113
- ```json
114
- {
115
- "tools": ["read", "write", "edit", "bash", "grep", "find", "ls"]
116
- }
117
- ```
118
-
119
- Add `"powershell"` on Windows if desired.
120
-
121
- ## Nerd Font spinner preset
122
-
123
- If your terminal uses a [Nerd Font](https://www.nerdfonts.com/), this preset enables all eight tools and the spinner shown in the demos:
124
-
125
- ```json
126
- {
127
- "$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/schemas/compact-tools.schema.json",
128
- "tools": ["read", "write", "edit", "bash", "powershell", "grep", "find", "ls"],
129
- "spinner": {
130
- "frames": ["✽", "✻", "✲", "✢", "✲", "✻"],
131
- "intervalMs": 80
132
- }
133
- }
134
- ```
135
-
136
- Remove `"powershell"` on systems where it is unavailable.
137
-
138
- ## Controls
139
-
140
- `Ctrl+T` cycles every thinking block through:
141
-
142
- ```text
143
- one-line summary → summary + │-indented detail → one-line summary → Thinking...
144
- ```
145
-
146
- Standalone bold lines and Markdown headings split one provider thinking run into logical sections. Each section gets its own summary and optional `│`-indented detail. The control row appears only on sections for which the provider returned expandable detail; when no thinking detail exists, no toggle hint is shown and `Ctrl+T` falls back to Pi's normal visible/hidden toggle instead of showing a duplicate detail phase. While thinking streams, a text-color highlight sweeps across summaries using streaming updates; no spinner glyph or title circle is shown. The transformation is display-only; complete thinking remains unchanged in the session and model context.
147
-
148
- Primary invocation targets stay visible, including paths, patterns, and shell commands. After every supported tool completes, the status reports the expanded result size as `Done in …s (N lines)` instead of repeating options such as context and limit. Counts use the returned text for reads, searches, listings, and shell commands, written content for `write`, and the rendered diff for `edit`. The count is computed once after completion and cached with the row, so streaming and repeated expansion do not add ongoing work. Large payloads represented as results, such as write content and edit diffs, follow the result toggle.
149
-
150
- A mouse click toggles one row's result:
151
-
152
- ```text
153
- result hidden ↔ full result
154
- ```
155
-
156
- `Ctrl+O` operates on all rows whose tool has `auto_compact: true`:
157
-
158
- - If any participating row is not fully expanded, it expands all participating rows.
159
- - If every participating row is fully expanded, it collapses all participating rows.
160
- - Rows whose tool has `auto_compact: false` are left unchanged.
161
-
162
- The extension handles and consumes `Ctrl+O` directly, so no Pi keybinding changes are required. With the default `edit: false`, edit diffs start visible and are excluded from the global toggle, while clicking an edit row still collapses or expands it.
163
-
164
- ## Notes
165
-
166
- - Pi `0.85.1+` and Node.js 20+ are recommended.
167
- - Fullscreen TUI mode is required for spinner animation and mouse interaction; `Ctrl+O` also works in regular mode.
168
- - Duration covers the full rendered tool-call lifecycle, including streamed `write` and `edit` arguments.
169
- - Selected tools override Pi's built-in definitions; other extensions overriding the same names may conflict by load order.
170
-
171
- ## Development
172
-
173
- ```bash
174
- npm install
175
- npm run check
176
- pi -e .
177
- ```
178
-
179
- Pi extensions execute with full system access. Review third-party extension source before installation.
180
-
181
- ## License
182
-
183
- MIT
1
+ # pi-compact-tools
2
+
3
+ Compact, expandable rendering for Pi's built-in tools, with a polished GitHub Dark theme.
4
+
5
+ > Formerly `@nedleeds/pi-compact-ui`. New installations should use `pi-compact-tools`.
6
+
7
+ ## Demo
8
+
9
+ The UI uses portable Unicode symbols and does not require a Nerd Font.
10
+
11
+ ### Compact tool workflow
12
+
13
+ Active, successful, and failed tool calls stay compact while preserving useful status and timing information.
14
+
15
+ ![Compact tool workflow with Nerd Font](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/nerd-font-workflow-optimized.gif)
16
+
17
+ ### Progress and duration display
18
+
19
+ The working label above the prompt uses the Thinking summary color with a soft white light sweep. Every tool state uses the same `⦁` glyph; running tools use a fourteen-frame cosine-eased RGB pulse between dim and bright gray, then settle to green on success or red on failure. Elapsed time remains visible in the neutral control color.
20
+
21
+ ![Spinner and elapsed-time display](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/spinner-duration-optimized.gif)
22
+
23
+ ### Bounded result previews
24
+
25
+ Completed `edit` rows show only the lines actually removed (`-`) or added (`+`), without unchanged context, hunk metadata, or success prose. The per-tool `auto_compact` setting chooses between hidden results and a bounded preview; click or `Ctrl+O` uses Pi's normal expansion flow to reveal all changed lines.
26
+
27
+ ![Edit diff](https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/assets/edit-diff.gif)
28
+
29
+ ## Features
30
+
31
+ - Compact rendering for `read`, `write`, `edit`, `bash`, `powershell`, `grep`, `find`, and `ls`
32
+ - One continuously glowing working label above the input prompt across thinking and tool execution
33
+ - Semantic built-in progress labels and automatically cleaned custom/MCP tool names without exposing invocation arguments
34
+ - Four-step `Ctrl+T` thinking cycle when provider detail is available: summary, detail, summary, then hidden
35
+ - Millisecond-precision duration and completed output line counts with neutral status text
36
+ - Per-tool hidden or persistent bounded-preview collapsed state with `auto_compact: true` or `false`
37
+ - Primary invocation targets such as paths and patterns stay visible; collapsed shell calls show concise intent summaries
38
+ - Click and `Ctrl+O` follow Pi's built-in expansion state while preview rows collapse back to their preview
39
+ - Edit patches display only removed (`-`) and added (`+`) lines, omitting unchanged context
40
+ - Configurable `previewLines` prevents large diffs, HTML, and shell output from flooding the transcript
41
+ - Width-cached expanded results, edit-diff processing, and unchanged result reuse during status animation
42
+ - Dark `│` and `└─` visual grouping
43
+ - Distinct cool-blue thinking text
44
+ - Truecolor and 256-color terminal support, plus Windows, Unix, and classic Mac line endings
45
+ - No Nerd Font requirement
46
+
47
+ ## Install
48
+
49
+ ```bash
50
+ pi install npm:pi-compact-tools
51
+ ```
52
+
53
+ Or install from GitHub:
54
+
55
+ ```bash
56
+ pi install git:github.com/nedleeds/pi-compact-tools
57
+ ```
58
+
59
+ Restart Pi or run `/reload`.
60
+
61
+ Migrating from the previous package name:
62
+
63
+ ```bash
64
+ pi remove npm:@nedleeds/pi-compact-ui
65
+ pi install npm:pi-compact-tools
66
+ ```
67
+
68
+ ## Theme
69
+
70
+ Select `github-dark-pro` in Pi's settings, or add it to `~/.pi/agent/settings.json`:
71
+
72
+ ```json
73
+ {
74
+ "theme": "github-dark-pro"
75
+ }
76
+ ```
77
+
78
+ ## Configuration
79
+
80
+ Configuration is optional. Create `~/.pi/agent/compact-tools.json` for global settings or `<project>/.pi/compact-tools.json` for a trusted project.
81
+
82
+ Default configuration:
83
+
84
+ ```json
85
+ {
86
+ "$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/schemas/compact-tools.schema.json",
87
+ "tools": ["read", "write", "edit", "bash"],
88
+ "previewLines": 10,
89
+ "auto_compact": {
90
+ "read": true,
91
+ "write": true,
92
+ "edit": false,
93
+ "bash": true
94
+ }
95
+ }
96
+ ```
97
+
98
+ Every tool call title uses `⦁`. Pending calls use dark gray; active calls smoothly pulse through fourteen cosine-eased RGB frames at a 45 ms interval without disappearing. Animation starts while arguments stream, so `write` and `edit` remain visibly active before their filesystem operation begins. Successful titles use green, and failed titles use red. `Done`, `Failed`, elapsed time, and line counts use the same neutral control color without printing inline click or keyboard hints. A single Pi working label animates independently of token arrival and reports `Thinking…`, `Responding…`, semantic built-in states such as `Reading file…`, or cleaned custom states such as `Using jira search…`. Invocation arguments remain in the tool row instead of being duplicated above the prompt. No separate spinner glyph is shown above the prompt.
99
+
100
+ `auto_compact` controls each tool's initial result state:
101
+
102
+ - `true` starts with the result hidden.
103
+ - `false` starts with at most `previewLines` rendered rows visible.
104
+ - Clicking or pressing `Ctrl+O` expands the complete result through Pi's built-in expansion state.
105
+
106
+ `previewLines` accepts `1`–`100` and defaults to `10`. It limits the collapsed preview; the complete result remains available. Unspecified entries inherit the previous configuration layer. By default, `read`, `write`, and `bash` start hidden, while `edit` starts with a short changed-lines preview.
107
+
108
+ To enable all Unix-compatible tools:
109
+
110
+ ```json
111
+ {
112
+ "tools": ["read", "write", "edit", "bash", "grep", "find", "ls"]
113
+ }
114
+ ```
115
+
116
+ Add `"powershell"` on Windows if desired.
117
+
118
+ ## Controls
119
+
120
+ `Ctrl+T` cycles every thinking block through:
121
+
122
+ ```text
123
+ one-line summary → summary + │-indented detail → one-line summary → Thinking...
124
+ ```
125
+
126
+ Standalone bold lines and Markdown headings split one provider thinking run into logical sections. Each section gets its own summary and optional `│`-indented detail. Summary-only views stay uncluttered with no control hint; the control row appears only while provider-supplied detail is visible. When no thinking detail exists, `Ctrl+T` falls back to Pi's normal visible/hidden toggle instead of showing a duplicate detail phase. While thinking streams, a slightly faster highlight sweeps from the Thinking summary color to white across Pi's working label at a steady interval rather than depending on token updates. The transcript summary remains visually stable. The transformation is display-only; complete thinking remains unchanged in the session and model context.
127
+
128
+ Primary file and search targets stay visible. Collapsed `bash` and `powershell` rows describe intent—such as `Run tests`, `Check repository status`, or `Search text`—without printing arguments; expanding the row reveals the exact command. After every supported tool completes, the status reports the expanded result size as `Done in …s (N lines)` instead of repeating options such as context and limit. Counts use the returned text for reads, searches, listings, and shell commands, written content for `write`, and only the displayed `-`/`+` lines for `edit`. The count is computed once after completion and cached with the row, so streaming and repeated expansion do not add ongoing work. Large payloads represented as results follow the result toggle.
129
+
130
+ A mouse click toggles one row using Pi's native result state. `Ctrl+O` remains owned by Pi and expands or collapses all tool rows consistently, including tools not rendered by this extension. A row with `auto_compact: false` cycles predictably between its bounded preview and the complete result, so repeated clicks never remove the row and fall through to a neighboring Thinking block. The extension does not intercept the global keybinding.
131
+
132
+ ## Notes
133
+
134
+ - Pi `0.85.1+` and Node.js `22.19.0+` are recommended.
135
+ - The working indicator and mouse interaction are provided by Pi's TUI; `Ctrl+O` also works in regular mode.
136
+ - Duration covers the full rendered tool-call lifecycle, including streamed `write` and `edit` arguments.
137
+ - Selected tools override definitions with the same names so their compact renderers are restored reliably after `/reload`; other extensions overriding those tools may conflict by load order.
138
+
139
+ ## Development
140
+
141
+ ```bash
142
+ npm install
143
+ npm run check
144
+ pi -e .
145
+ ```
146
+
147
+ Pi extensions execute with full system access. Review third-party extension source before installation.
148
+
149
+ ## License
150
+
151
+ MIT
@@ -1,14 +1,11 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/schemas/compact-tools.schema.json",
3
3
  "tools": ["read", "write", "edit", "bash"],
4
+ "previewLines": 10,
4
5
  "auto_compact": {
5
6
  "read": true,
6
7
  "write": true,
7
8
  "edit": false,
8
9
  "bash": true
9
- },
10
- "spinner": {
11
- "frames": ["●", "●", " ", "●", "●"],
12
- "intervalMs": 120
13
10
  }
14
11
  }
@@ -1,14 +1,11 @@
1
1
  {
2
2
  "$schema": "https://raw.githubusercontent.com/nedleeds/pi-compact-tools/main/schemas/compact-tools.schema.json",
3
3
  "tools": ["read", "write", "edit", "bash", "grep", "find", "ls"],
4
+ "previewLines": 10,
4
5
  "auto_compact": {
5
6
  "read": true,
6
7
  "write": true,
7
8
  "edit": false,
8
9
  "bash": true
9
- },
10
- "spinner": {
11
- "frames": ["◐", "◓", "◑", "◒"],
12
- "intervalMs": 120
13
10
  }
14
11
  }
@@ -0,0 +1,65 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+
3
+ export type Rgb = { r: number; g: number; b: number };
4
+ type ThemeForeground = Parameters<Theme["getFgAnsi"]>[0];
5
+
6
+ const ANSI_BASIC_RGB: readonly Rgb[] = [
7
+ { r: 0, g: 0, b: 0 }, { r: 128, g: 0, b: 0 }, { r: 0, g: 128, b: 0 }, { r: 128, g: 128, b: 0 },
8
+ { r: 0, g: 0, b: 128 }, { r: 128, g: 0, b: 128 }, { r: 0, g: 128, b: 128 }, { r: 192, g: 192, b: 192 },
9
+ { r: 128, g: 128, b: 128 }, { r: 255, g: 0, b: 0 }, { r: 0, g: 255, b: 0 }, { r: 255, g: 255, b: 0 },
10
+ { r: 0, g: 0, b: 255 }, { r: 255, g: 0, b: 255 }, { r: 0, g: 255, b: 255 }, { r: 255, g: 255, b: 255 },
11
+ ];
12
+
13
+ function ansi256ToRgb(index: number): Rgb {
14
+ if (index < 16) return ANSI_BASIC_RGB[index] ?? { r: 192, g: 192, b: 192 };
15
+ if (index < 232) {
16
+ const value = index - 16;
17
+ const channel = (part: number) => part === 0 ? 0 : 55 + part * 40;
18
+ return {
19
+ r: channel(Math.floor(value / 36)),
20
+ g: channel(Math.floor((value % 36) / 6)),
21
+ b: channel(value % 6),
22
+ };
23
+ }
24
+ const gray = 8 + Math.min(23, index - 232) * 10;
25
+ return { r: gray, g: gray, b: gray };
26
+ }
27
+
28
+ function rgbToAnsi256(rgb: Rgb): number {
29
+ let bestIndex = 0;
30
+ let bestDistance = Number.POSITIVE_INFINITY;
31
+ for (let index = 0; index < 256; index++) {
32
+ const candidate = ansi256ToRgb(index);
33
+ const distance = (rgb.r - candidate.r) ** 2 + (rgb.g - candidate.g) ** 2 + (rgb.b - candidate.b) ** 2;
34
+ if (distance >= bestDistance) continue;
35
+ bestIndex = index;
36
+ bestDistance = distance;
37
+ }
38
+ return bestIndex;
39
+ }
40
+
41
+ export function themeColorRgb(theme: Theme, color: ThemeForeground): Rgb | undefined {
42
+ if (typeof theme.getFgAnsi !== "function") return undefined;
43
+ const ansi = theme.getFgAnsi(color);
44
+ const trueColor = ansi.match(/\[38;2;(\d+);(\d+);(\d+)m/u);
45
+ if (trueColor) return { r: Number(trueColor[1]), g: Number(trueColor[2]), b: Number(trueColor[3]) };
46
+ const indexed = ansi.match(/\[38;5;(\d+)m/u);
47
+ return indexed ? ansi256ToRgb(Number(indexed[1])) : undefined;
48
+ }
49
+
50
+ export function interpolateRgb(from: Rgb, to: Rgb, amount: number): Rgb {
51
+ const bounded = Math.max(0, Math.min(1, amount));
52
+ const channel = (start: number, end: number) => Math.round(start + (end - start) * bounded);
53
+ return {
54
+ r: channel(from.r, to.r),
55
+ g: channel(from.g, to.g),
56
+ b: channel(from.b, to.b),
57
+ };
58
+ }
59
+
60
+ export function colorizeRgb(theme: Theme, color: Rgb, text: string): string {
61
+ if (typeof theme.getColorMode === "function" && theme.getColorMode() === "256color") {
62
+ return `\x1b[38;5;${rgbToAnsi256(color)}m${text}\x1b[39m`;
63
+ }
64
+ return `\x1b[38;2;${color.r};${color.g};${color.b}m${text}\x1b[39m`;
65
+ }
@@ -12,6 +12,7 @@ type JsonObject = Record<string, unknown>;
12
12
 
13
13
  export const DEFAULT_CONFIG: CompactToolsConfig = {
14
14
  tools: ["read", "write", "edit", "bash"],
15
+ previewLines: 10,
15
16
  auto_compact: {
16
17
  read: true,
17
18
  write: true,
@@ -22,10 +23,6 @@ export const DEFAULT_CONFIG: CompactToolsConfig = {
22
23
  find: true,
23
24
  ls: true,
24
25
  },
25
- spinner: {
26
- frames: ["◐", "◓", "◑", "◒"],
27
- intervalMs: 120,
28
- },
29
26
  };
30
27
 
31
28
  function isObject(value: unknown): value is JsonObject {
@@ -40,10 +37,6 @@ function isIntegerInRange(value: unknown, minimum: number, maximum: number): val
40
37
  return typeof value === "number" && Number.isInteger(value) && value >= minimum && value <= maximum;
41
38
  }
42
39
 
43
- function isNonEmptyStringArray(value: unknown): value is string[] {
44
- return Array.isArray(value) && value.length > 0 && value.every((item) => typeof item === "string" && item.length > 0);
45
- }
46
-
47
40
  function parseTools(value: unknown, path: string): CompactToolName[] | undefined {
48
41
  if (!Array.isArray(value)) {
49
42
  warn(path, "tools must be an array; using previous values");
@@ -82,28 +75,6 @@ function parseAutoCompact(
82
75
  return result;
83
76
  }
84
77
 
85
- function parseSpinner(
86
- base: CompactToolsConfig["spinner"],
87
- value: unknown,
88
- path: string,
89
- ): CompactToolsConfig["spinner"] {
90
- if (value === undefined) return base;
91
- if (!isObject(value)) {
92
- warn(path, "spinner must be an object; using previous values");
93
- return base;
94
- }
95
- const frames = value.frames;
96
- const intervalMs = value.intervalMs;
97
- const validFrames = isNonEmptyStringArray(frames);
98
- const validInterval = isIntegerInRange(intervalMs, 40, 5_000);
99
- if (frames !== undefined && !validFrames) warn(path, "spinner.frames must be a non-empty string array");
100
- if (intervalMs !== undefined && !validInterval) warn(path, "spinner.intervalMs must be 40–5000");
101
- return {
102
- frames: validFrames ? frames : base.frames,
103
- intervalMs: validInterval ? intervalMs : base.intervalMs,
104
- };
105
- }
106
-
107
78
  export function mergeConfig(base: CompactToolsConfig, value: unknown, path: string): CompactToolsConfig {
108
79
  if (value === undefined) return base;
109
80
  if (!isObject(value)) {
@@ -112,8 +83,12 @@ export function mergeConfig(base: CompactToolsConfig, value: unknown, path: stri
112
83
  }
113
84
  const tools = value.tools === undefined ? base.tools : (parseTools(value.tools, path) ?? base.tools);
114
85
  const auto_compact = parseAutoCompact(base.auto_compact, value.auto_compact, path);
115
- const spinner = parseSpinner(base.spinner, value.spinner, path);
116
- return { tools, auto_compact, spinner };
86
+ const previewLines = value.previewLines === undefined ? base.previewLines
87
+ : isIntegerInRange(value.previewLines, 1, 100) ? value.previewLines : base.previewLines;
88
+ if (value.previewLines !== undefined && !isIntegerInRange(value.previewLines, 1, 100)) {
89
+ warn(path, "previewLines must be 1–100; using previous value");
90
+ }
91
+ return { tools, auto_compact, previewLines };
117
92
  }
118
93
 
119
94
  function readJson(path: string): unknown {
@@ -133,13 +108,3 @@ export function loadConfig(cwd?: string, projectTrusted = false): CompactToolsCo
133
108
  const projectPath = join(cwd, CONFIG_DIR_NAME, CONFIG_FILE);
134
109
  return mergeConfig(globalConfig, readJson(projectPath), projectPath);
135
110
  }
136
-
137
- export function isFullscreenMode(argv = process.argv): boolean {
138
- for (let index = 0; index < argv.length; index++) {
139
- const argument = argv[index];
140
- if (argument === "--tui-mode") return argv[index + 1] === "fullscreen";
141
- if (argument?.startsWith("--tui-mode=")) return argument.slice("--tui-mode=".length) === "fullscreen";
142
- }
143
- const settings = readJson(join(getAgentDir(), "settings.json"));
144
- return isObject(settings) && settings.tuiMode === "fullscreen";
145
- }
@@ -2,6 +2,32 @@
2
2
 
3
3
  export type RowStatus = "pending" | "running" | "success" | "error";
4
4
 
5
+ export type IndicatorTone = "borderAccent" | "border" | "borderMuted" | "success" | "error";
6
+
7
+ export const RUNNING_INDICATOR_FRAME_COUNT = 14;
8
+ const RUNNING_INDICATOR_MIN_STRENGTH = 0.08;
9
+
10
+ export function indicatorStrength(status: RowStatus, frame = 0): number {
11
+ if (status !== "running") return 1;
12
+ const normalizedFrame = ((frame % RUNNING_INDICATOR_FRAME_COUNT) + RUNNING_INDICATOR_FRAME_COUNT)
13
+ % RUNNING_INDICATOR_FRAME_COUNT;
14
+ const wave = (Math.cos((normalizedFrame / RUNNING_INDICATOR_FRAME_COUNT) * Math.PI * 2) + 1) / 2;
15
+ return RUNNING_INDICATOR_MIN_STRENGTH + (1 - RUNNING_INDICATOR_MIN_STRENGTH) * wave;
16
+ }
17
+
18
+ /** Semantic fallback for themes whose foreground RGB values cannot be resolved. */
19
+ export function indicatorTone(status: RowStatus, frame = 0): IndicatorTone {
20
+ if (status === "success") return "success";
21
+ if (status === "error") return "error";
22
+ if (status === "pending") return "borderAccent";
23
+ const strength = indicatorStrength(status, frame);
24
+ return strength >= 0.7 ? "borderAccent" : strength >= 0.35 ? "border" : "borderMuted";
25
+ }
26
+
27
+ export function indicatorGlyph(_status: RowStatus, _frame = 0): string {
28
+ return "⦁";
29
+ }
30
+
5
31
  export function normalizeLineEndings(value: string): string {
6
32
  return value.replace(/\r\n?|\n/g, "\n");
7
33
  }