@oh-my-pi/pi-coding-agent 3.0.1337 → 3.1.1337
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/package.json +4 -4
- package/src/core/skills.ts +54 -68
- package/src/core/tools/task/agents.ts +21 -0
- package/src/core/tools/task/bundled-agents/reviewer.md +2 -1
- package/src/core/tools/task/bundled-agents/task.md +1 -0
- package/src/core/tools/task/discovery.ts +21 -0
- package/src/core/tools/task/executor.ts +16 -1
- package/src/core/tools/task/index.ts +25 -0
- package/src/core/tools/task/types.ts +4 -0
- package/src/modes/interactive/interactive-mode.ts +36 -14
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.1.1337] - 2026-01-03
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `spawns` frontmatter field for agent definitions to control which sub-agents can be spawned
|
|
9
|
+
- Added spawn restriction enforcement preventing agents from spawning unauthorized sub-agents
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Fixed duplicate skill loading when the same SKILL.md file was discovered through multiple paths
|
|
14
|
+
|
|
5
15
|
## [3.0.1337] - 2026-01-03
|
|
6
16
|
|
|
7
17
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1337",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
43
|
-
"@oh-my-pi/pi-ai": "3.
|
|
44
|
-
"@oh-my-pi/pi-tui": "3.
|
|
42
|
+
"@oh-my-pi/pi-agent-core": "3.1.1337",
|
|
43
|
+
"@oh-my-pi/pi-ai": "3.1.1337",
|
|
44
|
+
"@oh-my-pi/pi-tui": "3.1.1337",
|
|
45
45
|
"@sinclair/typebox": "^0.34.46",
|
|
46
46
|
"ajv": "^8.17.1",
|
|
47
47
|
"chalk": "^5.5.0",
|
package/src/core/skills.ts
CHANGED
|
@@ -46,6 +46,30 @@ export interface LoadSkillsFromDirOptions {
|
|
|
46
46
|
export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult {
|
|
47
47
|
const skills: Skill[] = [];
|
|
48
48
|
const warnings: SkillWarning[] = [];
|
|
49
|
+
const seenPaths = new Set<string>();
|
|
50
|
+
|
|
51
|
+
function addSkill(skillFile: string, skillDir: string, dirName: string) {
|
|
52
|
+
if (seenPaths.has(skillFile)) return;
|
|
53
|
+
try {
|
|
54
|
+
const content = readFileSync(skillFile, "utf-8");
|
|
55
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
56
|
+
const name = (frontmatter.name as string) || dirName;
|
|
57
|
+
const description = frontmatter.description as string;
|
|
58
|
+
|
|
59
|
+
if (description) {
|
|
60
|
+
seenPaths.add(skillFile);
|
|
61
|
+
skills.push({
|
|
62
|
+
name,
|
|
63
|
+
description,
|
|
64
|
+
filePath: skillFile,
|
|
65
|
+
baseDir: skillDir,
|
|
66
|
+
source: options.source,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
} catch {
|
|
70
|
+
// Skip invalid skills
|
|
71
|
+
}
|
|
72
|
+
}
|
|
49
73
|
|
|
50
74
|
function scanDir(dir: string) {
|
|
51
75
|
try {
|
|
@@ -59,45 +83,14 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills
|
|
|
59
83
|
try {
|
|
60
84
|
const stat = statSync(skillFile);
|
|
61
85
|
if (stat.isFile()) {
|
|
62
|
-
|
|
63
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
64
|
-
const name = (frontmatter.name as string) || entry.name;
|
|
65
|
-
const description = frontmatter.description as string;
|
|
66
|
-
|
|
67
|
-
if (description) {
|
|
68
|
-
skills.push({
|
|
69
|
-
name,
|
|
70
|
-
description,
|
|
71
|
-
filePath: skillFile,
|
|
72
|
-
baseDir: fullPath,
|
|
73
|
-
source: options.source,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
86
|
+
addSkill(skillFile, fullPath, entry.name);
|
|
76
87
|
}
|
|
77
88
|
} catch {
|
|
78
|
-
//
|
|
89
|
+
// No SKILL.md in this directory
|
|
79
90
|
}
|
|
80
|
-
|
|
81
91
|
scanDir(fullPath);
|
|
82
92
|
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
|
83
|
-
|
|
84
|
-
const content = readFileSync(fullPath, "utf-8");
|
|
85
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
86
|
-
const name = (frontmatter.name as string) || basename(dir);
|
|
87
|
-
const description = frontmatter.description as string;
|
|
88
|
-
|
|
89
|
-
if (description) {
|
|
90
|
-
skills.push({
|
|
91
|
-
name,
|
|
92
|
-
description,
|
|
93
|
-
filePath: fullPath,
|
|
94
|
-
baseDir: dir,
|
|
95
|
-
source: options.source,
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
} catch {
|
|
99
|
-
// Skip invalid skills
|
|
100
|
-
}
|
|
93
|
+
addSkill(fullPath, dir, basename(dir));
|
|
101
94
|
}
|
|
102
95
|
}
|
|
103
96
|
} catch (err) {
|
|
@@ -117,6 +110,30 @@ export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkills
|
|
|
117
110
|
function scanDirectoryForSkills(dir: string): LoadSkillsResult {
|
|
118
111
|
const skills: Skill[] = [];
|
|
119
112
|
const warnings: SkillWarning[] = [];
|
|
113
|
+
const seenPaths = new Set<string>();
|
|
114
|
+
|
|
115
|
+
function addSkill(skillFile: string, skillDir: string, dirName: string) {
|
|
116
|
+
if (seenPaths.has(skillFile)) return;
|
|
117
|
+
try {
|
|
118
|
+
const content = readFileSync(skillFile, "utf-8");
|
|
119
|
+
const { frontmatter } = parseFrontmatter(content);
|
|
120
|
+
const name = (frontmatter.name as string) || dirName;
|
|
121
|
+
const description = frontmatter.description as string;
|
|
122
|
+
|
|
123
|
+
if (description) {
|
|
124
|
+
seenPaths.add(skillFile);
|
|
125
|
+
skills.push({
|
|
126
|
+
name,
|
|
127
|
+
description,
|
|
128
|
+
filePath: skillFile,
|
|
129
|
+
baseDir: skillDir,
|
|
130
|
+
source: "custom",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// Skip invalid skills
|
|
135
|
+
}
|
|
136
|
+
}
|
|
120
137
|
|
|
121
138
|
function scanDir(currentDir: string) {
|
|
122
139
|
try {
|
|
@@ -130,45 +147,14 @@ function scanDirectoryForSkills(dir: string): LoadSkillsResult {
|
|
|
130
147
|
try {
|
|
131
148
|
const stat = statSync(skillFile);
|
|
132
149
|
if (stat.isFile()) {
|
|
133
|
-
|
|
134
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
135
|
-
const name = (frontmatter.name as string) || entry.name;
|
|
136
|
-
const description = frontmatter.description as string;
|
|
137
|
-
|
|
138
|
-
if (description) {
|
|
139
|
-
skills.push({
|
|
140
|
-
name,
|
|
141
|
-
description,
|
|
142
|
-
filePath: skillFile,
|
|
143
|
-
baseDir: fullPath,
|
|
144
|
-
source: "custom",
|
|
145
|
-
});
|
|
146
|
-
}
|
|
150
|
+
addSkill(skillFile, fullPath, entry.name);
|
|
147
151
|
}
|
|
148
152
|
} catch {
|
|
149
|
-
//
|
|
153
|
+
// No SKILL.md in this directory
|
|
150
154
|
}
|
|
151
|
-
|
|
152
155
|
scanDir(fullPath);
|
|
153
156
|
} else if (entry.isFile() && entry.name === "SKILL.md") {
|
|
154
|
-
|
|
155
|
-
const content = readFileSync(fullPath, "utf-8");
|
|
156
|
-
const { frontmatter } = parseFrontmatter(content);
|
|
157
|
-
const name = (frontmatter.name as string) || basename(currentDir);
|
|
158
|
-
const description = frontmatter.description as string;
|
|
159
|
-
|
|
160
|
-
if (description) {
|
|
161
|
-
skills.push({
|
|
162
|
-
name,
|
|
163
|
-
description,
|
|
164
|
-
filePath: fullPath,
|
|
165
|
-
baseDir: currentDir,
|
|
166
|
-
source: "custom",
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
} catch {
|
|
170
|
-
// Skip invalid skills
|
|
171
|
-
}
|
|
157
|
+
addSkill(fullPath, currentDir, basename(currentDir));
|
|
172
158
|
}
|
|
173
159
|
}
|
|
174
160
|
} catch (err) {
|
|
@@ -68,6 +68,26 @@ function parseAgent(fileName: string, content: string, source: AgentSource): Age
|
|
|
68
68
|
.map((t) => t.trim())
|
|
69
69
|
.filter(Boolean);
|
|
70
70
|
|
|
71
|
+
// Parse spawns field
|
|
72
|
+
let spawns: string[] | "*" | undefined;
|
|
73
|
+
if (frontmatter.spawns !== undefined) {
|
|
74
|
+
const spawnsRaw = frontmatter.spawns.trim();
|
|
75
|
+
if (spawnsRaw === "*") {
|
|
76
|
+
spawns = "*";
|
|
77
|
+
} else if (spawnsRaw) {
|
|
78
|
+
spawns = spawnsRaw
|
|
79
|
+
.split(",")
|
|
80
|
+
.map((s) => s.trim())
|
|
81
|
+
.filter(Boolean);
|
|
82
|
+
if (spawns.length === 0) spawns = undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Backward compat: infer spawns: "*" when tools includes "task"
|
|
87
|
+
if (spawns === undefined && tools?.includes("task")) {
|
|
88
|
+
spawns = "*";
|
|
89
|
+
}
|
|
90
|
+
|
|
71
91
|
const recursive =
|
|
72
92
|
frontmatter.recursive === undefined ? false : frontmatter.recursive === "true" || frontmatter.recursive === "1";
|
|
73
93
|
|
|
@@ -75,6 +95,7 @@ function parseAgent(fileName: string, content: string, source: AgentSource): Age
|
|
|
75
95
|
name: frontmatter.name,
|
|
76
96
|
description: frontmatter.description,
|
|
77
97
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
98
|
+
spawns,
|
|
78
99
|
model: frontmatter.model,
|
|
79
100
|
recursive,
|
|
80
101
|
systemPrompt: body,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: reviewer
|
|
3
3
|
description: Code review specialist for quality and security analysis
|
|
4
|
-
tools: read, grep, find, ls, bash,
|
|
4
|
+
tools: read, grep, find, ls, bash, report_finding, submit_review
|
|
5
|
+
spawns: explore
|
|
5
6
|
model: pi/slow, gpt-5.2-codex, gpt-5.2, codex, gpt
|
|
6
7
|
---
|
|
7
8
|
|
|
@@ -106,6 +106,26 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
|
|
|
106
106
|
.map((t) => t.trim())
|
|
107
107
|
.filter(Boolean);
|
|
108
108
|
|
|
109
|
+
// Parse spawns field
|
|
110
|
+
let spawns: string[] | "*" | undefined;
|
|
111
|
+
if (frontmatter.spawns !== undefined) {
|
|
112
|
+
const spawnsRaw = frontmatter.spawns.trim();
|
|
113
|
+
if (spawnsRaw === "*") {
|
|
114
|
+
spawns = "*";
|
|
115
|
+
} else if (spawnsRaw) {
|
|
116
|
+
spawns = spawnsRaw
|
|
117
|
+
.split(",")
|
|
118
|
+
.map((s) => s.trim())
|
|
119
|
+
.filter(Boolean);
|
|
120
|
+
if (spawns.length === 0) spawns = undefined;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Backward compat: infer spawns: "*" when tools includes "task"
|
|
125
|
+
if (spawns === undefined && tools?.includes("task")) {
|
|
126
|
+
spawns = "*";
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
const recursive =
|
|
110
130
|
frontmatter.recursive === undefined
|
|
111
131
|
? undefined
|
|
@@ -115,6 +135,7 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentDefinition[]
|
|
|
115
135
|
name: frontmatter.name,
|
|
116
136
|
description: frontmatter.description,
|
|
117
137
|
tools: tools && tools.length > 0 ? tools : undefined,
|
|
138
|
+
spawns,
|
|
118
139
|
model: frontmatter.model,
|
|
119
140
|
recursive,
|
|
120
141
|
systemPrompt: body,
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
MAX_OUTPUT_BYTES,
|
|
20
20
|
MAX_OUTPUT_LINES,
|
|
21
21
|
OMP_BLOCKED_AGENT_ENV,
|
|
22
|
+
OMP_SPAWNS_ENV,
|
|
22
23
|
type SingleResult,
|
|
23
24
|
} from "./types";
|
|
24
25
|
|
|
@@ -193,7 +194,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
193
194
|
|
|
194
195
|
// Add tools if specified
|
|
195
196
|
if (agent.tools && agent.tools.length > 0) {
|
|
196
|
-
|
|
197
|
+
let toolList = agent.tools;
|
|
198
|
+
// Auto-include task tool if spawns defined but task not in tools
|
|
199
|
+
if (agent.spawns !== undefined && !toolList.includes("task")) {
|
|
200
|
+
toolList = [...toolList, "task"];
|
|
201
|
+
}
|
|
202
|
+
args.push("--tools", toolList.join(","));
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
// Resolve and add model
|
|
@@ -220,6 +226,15 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
220
226
|
env[OMP_BLOCKED_AGENT_ENV] = agent.name;
|
|
221
227
|
}
|
|
222
228
|
|
|
229
|
+
// Propagate spawn restrictions to subprocess
|
|
230
|
+
if (agent.spawns === undefined) {
|
|
231
|
+
env[OMP_SPAWNS_ENV] = ""; // No spawns = deny all
|
|
232
|
+
} else if (agent.spawns === "*") {
|
|
233
|
+
env[OMP_SPAWNS_ENV] = "*";
|
|
234
|
+
} else {
|
|
235
|
+
env[OMP_SPAWNS_ENV] = agent.spawns.join(",");
|
|
236
|
+
}
|
|
237
|
+
|
|
223
238
|
// Spawn subprocess
|
|
224
239
|
const proc = spawn(OMP_CMD, args, {
|
|
225
240
|
cwd,
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
MAX_PARALLEL_TASKS,
|
|
28
28
|
OMP_BLOCKED_AGENT_ENV,
|
|
29
29
|
OMP_NO_SUBAGENTS_ENV,
|
|
30
|
+
OMP_SPAWNS_ENV,
|
|
30
31
|
type TaskToolDetails,
|
|
31
32
|
taskSchema,
|
|
32
33
|
} from "./types";
|
|
@@ -321,6 +322,30 @@ export function createTaskTool(
|
|
|
321
322
|
}
|
|
322
323
|
}
|
|
323
324
|
|
|
325
|
+
// Check spawn restrictions from parent
|
|
326
|
+
const parentSpawns = process.env[OMP_SPAWNS_ENV];
|
|
327
|
+
const isSpawnAllowed = (agentName: string): boolean => {
|
|
328
|
+
if (parentSpawns === undefined) return true; // Root = allow all
|
|
329
|
+
if (parentSpawns === "") return false; // Empty = deny all
|
|
330
|
+
if (parentSpawns === "*") return true; // Wildcard = allow all
|
|
331
|
+
const allowed = new Set(parentSpawns.split(",").map((s) => s.trim()));
|
|
332
|
+
return allowed.has(agentName);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
for (const task of tasks) {
|
|
336
|
+
if (!isSpawnAllowed(task.agent)) {
|
|
337
|
+
const allowed = parentSpawns === "" ? "none (spawns disabled for this agent)" : parentSpawns;
|
|
338
|
+
return {
|
|
339
|
+
content: [{ type: "text", text: `Cannot spawn '${task.agent}'. Allowed: ${allowed}` }],
|
|
340
|
+
details: {
|
|
341
|
+
projectAgentsDir,
|
|
342
|
+
results: [],
|
|
343
|
+
totalDurationMs: Date.now() - startTime,
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
324
349
|
// Initialize progress for all tasks
|
|
325
350
|
for (let i = 0; i < tasks.length; i++) {
|
|
326
351
|
const agentCfg = getAgent(agents, tasks[i].agent);
|
|
@@ -33,6 +33,9 @@ export const OMP_NO_SUBAGENTS_ENV = "OMP_NO_SUBAGENTS";
|
|
|
33
33
|
/** Environment variable containing blocked agent name (self-recursion prevention) */
|
|
34
34
|
export const OMP_BLOCKED_AGENT_ENV = "OMP_BLOCKED_AGENT";
|
|
35
35
|
|
|
36
|
+
/** Environment variable containing allowed spawn list (propagated to subprocesses) */
|
|
37
|
+
export const OMP_SPAWNS_ENV = "OMP_SPAWNS";
|
|
38
|
+
|
|
36
39
|
/** Task tool parameters */
|
|
37
40
|
export const taskSchema = Type.Object({
|
|
38
41
|
context: Type.Optional(Type.String({ description: "Shared context prepended to all task prompts" })),
|
|
@@ -74,6 +77,7 @@ export interface AgentDefinition {
|
|
|
74
77
|
description: string;
|
|
75
78
|
systemPrompt: string;
|
|
76
79
|
tools?: string[];
|
|
80
|
+
spawns?: string[] | "*";
|
|
77
81
|
model?: string;
|
|
78
82
|
recursive?: boolean;
|
|
79
83
|
source: AgentSource;
|
|
@@ -2414,10 +2414,30 @@ export class InteractiveMode {
|
|
|
2414
2414
|
|
|
2415
2415
|
const renderSourceText = (text: string): string => text.replace(/\bvia\b/, theme.italic("via"));
|
|
2416
2416
|
|
|
2417
|
-
const truncateText = (text: string,
|
|
2418
|
-
|
|
2419
|
-
if (
|
|
2420
|
-
|
|
2417
|
+
const truncateText = (text: string, maxWidth: number): string => {
|
|
2418
|
+
const textWidth = visibleWidth(text);
|
|
2419
|
+
if (textWidth <= maxWidth) return text;
|
|
2420
|
+
if (maxWidth <= 3) {
|
|
2421
|
+
let acc = "";
|
|
2422
|
+
let width = 0;
|
|
2423
|
+
for (const char of text) {
|
|
2424
|
+
const charWidth = visibleWidth(char);
|
|
2425
|
+
if (width + charWidth > maxWidth) break;
|
|
2426
|
+
width += charWidth;
|
|
2427
|
+
acc += char;
|
|
2428
|
+
}
|
|
2429
|
+
return acc;
|
|
2430
|
+
}
|
|
2431
|
+
const targetWidth = maxWidth - 3;
|
|
2432
|
+
let acc = "";
|
|
2433
|
+
let width = 0;
|
|
2434
|
+
for (const char of text) {
|
|
2435
|
+
const charWidth = visibleWidth(char);
|
|
2436
|
+
if (width + charWidth > targetWidth) break;
|
|
2437
|
+
width += charWidth;
|
|
2438
|
+
acc += char;
|
|
2439
|
+
}
|
|
2440
|
+
return `${acc}...`;
|
|
2421
2441
|
};
|
|
2422
2442
|
|
|
2423
2443
|
// Helper to format a section with consistent column alignment
|
|
@@ -2438,26 +2458,28 @@ export class InteractiveMode {
|
|
|
2438
2458
|
return { name, sourceText, nameWithSource, desc };
|
|
2439
2459
|
});
|
|
2440
2460
|
|
|
2441
|
-
const maxNameWidth = Math.min(
|
|
2461
|
+
const maxNameWidth = Math.min(
|
|
2462
|
+
60,
|
|
2463
|
+
Math.max(...lineItems.map((line) => visibleWidth(line.nameWithSource))),
|
|
2464
|
+
);
|
|
2442
2465
|
const formattedLines = lineItems.map((line) => {
|
|
2443
2466
|
let nameText = line.name;
|
|
2444
2467
|
let sourceText = line.sourceText;
|
|
2445
2468
|
|
|
2446
2469
|
if (sourceText) {
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
sourceText = truncateText(sourceText, Math.max(0, maxNameWidth - 4));
|
|
2450
|
-
availableForName = maxNameWidth - sourceText.length - 1;
|
|
2451
|
-
}
|
|
2452
|
-
nameText = truncateText(nameText, Math.max(1, availableForName));
|
|
2453
|
-
} else {
|
|
2454
|
-
nameText = truncateText(nameText, maxNameWidth);
|
|
2470
|
+
const maxSourceWidth = Math.max(0, maxNameWidth - 2);
|
|
2471
|
+
sourceText = truncateText(sourceText, maxSourceWidth);
|
|
2455
2472
|
}
|
|
2473
|
+
const sourceWidth = sourceText ? visibleWidth(sourceText) : 0;
|
|
2474
|
+
const availableForName = sourceText
|
|
2475
|
+
? Math.max(1, maxNameWidth - sourceWidth - 1)
|
|
2476
|
+
: maxNameWidth;
|
|
2477
|
+
nameText = truncateText(nameText, availableForName);
|
|
2456
2478
|
|
|
2457
2479
|
const nameWithSourcePlain = sourceText ? `${nameText} ${sourceText}` : nameText;
|
|
2458
2480
|
const sourceRendered = sourceText ? renderSourceText(sourceText) : "";
|
|
2459
2481
|
const nameRendered = sourceText ? `${theme.bold(nameText)} ${sourceRendered}` : theme.bold(nameText);
|
|
2460
|
-
const pad = Math.max(0, maxNameWidth - nameWithSourcePlain
|
|
2482
|
+
const pad = Math.max(0, maxNameWidth - visibleWidth(nameWithSourcePlain));
|
|
2461
2483
|
const desc = line.desc?.trim();
|
|
2462
2484
|
const descPart = desc ? ` ${theme.fg("dim", desc.slice(0, 50) + (desc.length > 50 ? "..." : ""))}` : "";
|
|
2463
2485
|
return ` ${nameRendered}${" ".repeat(pad)}${descPart}`;
|