pi-crew 0.1.16 → 0.1.17
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 +7 -0
- package/package.json +1 -1
- package/src/extension/register.ts +7 -5
- package/src/extension/team-tool.ts +1 -1
- package/src/runtime/child-pi.ts +21 -3
- package/src/runtime/crew-agent-records.ts +8 -0
- package/src/runtime/crew-agent-runtime.ts +3 -0
- package/src/runtime/pi-json-output.ts +3 -2
- package/src/runtime/policy-engine.ts +1 -1
- package/src/runtime/task-runner.ts +19 -2
- package/src/ui/run-dashboard.ts +13 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.17
|
|
4
|
+
|
|
5
|
+
- Fixed terminal/completed workers being incorrectly escalated as stale heartbeat blockers after all tasks completed.
|
|
6
|
+
- Cleaned child-process result extraction so result artifacts prefer final assistant output and no longer include worker prompt/context.
|
|
7
|
+
- Made `/team-dashboard` visibly render as a top-right sidebar by default with explicit right-sidebar title text.
|
|
8
|
+
- Added per-subagent model and usage fields to agent records, status output, and dashboard fallbacks so model/token totals stay visible while and after workers run.
|
|
9
|
+
|
|
3
10
|
## 0.1.16
|
|
4
11
|
|
|
5
12
|
- Added right-side `/team-dashboard` placement with model, token, and tool detail rows for subagents.
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@ import { notifyActiveRuns } from "./session-summary.ts";
|
|
|
7
7
|
import { piTeamsHelp } from "./help.ts";
|
|
8
8
|
import { handleTeamManagerCommand } from "./team-manager-command.ts";
|
|
9
9
|
import { handleTeamTool, type TeamToolDetails } from "./team-tool.ts";
|
|
10
|
-
import {
|
|
10
|
+
import { listRecentRuns } from "./run-index.ts";
|
|
11
11
|
import { RunDashboard, type RunDashboardSelection } from "../ui/run-dashboard.ts";
|
|
12
12
|
import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.ts";
|
|
13
13
|
import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
|
|
@@ -381,13 +381,15 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
381
381
|
description: "Open a pi-crew run dashboard overlay",
|
|
382
382
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
383
383
|
for (;;) {
|
|
384
|
-
const runs =
|
|
384
|
+
const runs = listRecentRuns(ctx.cwd, 50);
|
|
385
385
|
const uiConfig = loadConfig(ctx.cwd).config.ui;
|
|
386
386
|
const rightPanel = uiConfig?.dashboardPlacement !== "center";
|
|
387
|
-
const width = rightPanel ? Math.min(
|
|
388
|
-
const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools }), {
|
|
387
|
+
const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56)) : "90%";
|
|
388
|
+
const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools }), {
|
|
389
389
|
overlay: true,
|
|
390
|
-
overlayOptions:
|
|
390
|
+
overlayOptions: rightPanel
|
|
391
|
+
? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } }
|
|
392
|
+
: { width, maxHeight: "90%", anchor: "center", margin: 2 },
|
|
391
393
|
});
|
|
392
394
|
if (!selection) return;
|
|
393
395
|
if (selection.action === "reload") continue;
|
|
@@ -384,7 +384,7 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
384
384
|
...(tasks.length ? tasks.map((task) => `- ${task.id} [${task.status}] ${task.role} -> ${task.agent}${task.taskPacket ? ` scope=${task.taskPacket.scope}` : ""}${task.verification ? ` green=${task.verification.observedGreenLevel}/${task.verification.requiredGreenLevel}` : ""}${task.modelAttempts?.length ? ` attempts=${task.modelAttempts.length}` : ""}${task.jsonEvents !== undefined ? ` jsonEvents=${task.jsonEvents}` : ""}${task.usage ? ` usage=${JSON.stringify(task.usage)}` : ""}${task.worktree ? ` worktree=${task.worktree.path}` : ""}${task.error ? ` error=${task.error}` : ""}`) : ["- (none)"]),
|
|
385
385
|
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
386
386
|
"Agents:",
|
|
387
|
-
...(crewAgents.length ? crewAgents.map((agent) => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.progress?.activityState === "needs_attention" ? " needs_attention" : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`) : ["- (none)"]),
|
|
387
|
+
...(crewAgents.length ? crewAgents.map((agent) => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.progress?.activityState === "needs_attention" ? " needs_attention" : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`) : ["- (none)"]),
|
|
388
388
|
"Policy decisions:",
|
|
389
389
|
...(manifest.policyDecisions?.length ? manifest.policyDecisions.map((item) => `- ${item.action} (${item.reason})${item.taskId ? ` ${item.taskId}` : ""}: ${item.message}`) : ["- (none)"]),
|
|
390
390
|
`Total usage: ${formatUsage(totalUsage)}`,
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -9,6 +9,7 @@ const POST_EXIT_STDIO_GUARD_MS = 3000;
|
|
|
9
9
|
const FINAL_DRAIN_MS = 5000;
|
|
10
10
|
const HARD_KILL_MS = 3000;
|
|
11
11
|
const MAX_CAPTURE_BYTES = 256 * 1024;
|
|
12
|
+
const MAX_COMPACT_CONTENT_CHARS = 4096;
|
|
12
13
|
const activeChildProcesses = new Map<number, ChildProcess>();
|
|
13
14
|
|
|
14
15
|
function appendBoundedTail(current: string, chunk: string, maxBytes = MAX_CAPTURE_BYTES): string {
|
|
@@ -68,12 +69,27 @@ function appendTranscript(input: ChildPiRunInput, line: string): void {
|
|
|
68
69
|
fs.appendFileSync(input.transcriptPath, `${line}\n`, "utf-8");
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
function compactString(value: string, maxChars = MAX_COMPACT_CONTENT_CHARS): string {
|
|
73
|
+
if (value.length <= maxChars) return value;
|
|
74
|
+
return `${value.slice(0, maxChars)}\n[pi-crew compacted ${value.length - maxChars} chars]`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function compactValue(value: unknown): unknown {
|
|
78
|
+
if (typeof value === "string") return compactString(value);
|
|
79
|
+
if (Array.isArray(value)) return value.slice(0, 20).map(compactValue);
|
|
80
|
+
const record = asRecord(value);
|
|
81
|
+
if (!record) return value;
|
|
82
|
+
const compacted: Record<string, unknown> = {};
|
|
83
|
+
for (const [key, entry] of Object.entries(record).slice(0, 20)) compacted[key] = compactValue(entry);
|
|
84
|
+
return compacted;
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
function compactContentPart(part: unknown): unknown | undefined {
|
|
72
88
|
const record = asRecord(part);
|
|
73
89
|
if (!record) return undefined;
|
|
74
|
-
if (record.type === "text") return { type: "text", text: typeof record.text === "string" ? record.text : "" };
|
|
75
|
-
if (record.type === "toolCall") return { type: "toolCall", name: record.name, input: record.input };
|
|
76
|
-
if (record.type === "toolResult") return { type: "toolResult", name: record.name, content: record.content };
|
|
90
|
+
if (record.type === "text") return { type: "text", text: typeof record.text === "string" ? compactString(record.text) : "" };
|
|
91
|
+
if (record.type === "toolCall") return { type: "toolCall", name: record.name, input: compactValue(record.input) };
|
|
92
|
+
if (record.type === "toolResult") return { type: "toolResult", name: record.name, content: compactValue(record.content) };
|
|
77
93
|
return undefined;
|
|
78
94
|
}
|
|
79
95
|
|
|
@@ -106,7 +122,9 @@ function displayTextFromCompactEvent(event: unknown): string | undefined {
|
|
|
106
122
|
if (record.type === "tool_execution_start") {
|
|
107
123
|
return typeof record.toolName === "string" ? `tool: ${record.toolName}` : "tool started";
|
|
108
124
|
}
|
|
125
|
+
if (record.type !== "message" && record.type !== "message_end") return undefined;
|
|
109
126
|
const message = asRecord(record.message);
|
|
127
|
+
if (message?.role !== undefined && message.role !== "assistant") return undefined;
|
|
110
128
|
const content = Array.isArray(message?.content) ? message.content : [];
|
|
111
129
|
const text = content.flatMap((part) => {
|
|
112
130
|
const item = asRecord(part);
|
|
@@ -124,6 +124,12 @@ export function emptyCrewAgentProgress(): CrewAgentProgress {
|
|
|
124
124
|
return { recentTools: [], recentOutput: [], toolCount: 0 };
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
+
function modelFromTask(task: TeamTaskState): string | undefined {
|
|
128
|
+
const attempts = task.modelAttempts;
|
|
129
|
+
if (!attempts?.length) return undefined;
|
|
130
|
+
return attempts.find((attempt) => attempt.success)?.model ?? attempts.at(-1)?.model;
|
|
131
|
+
}
|
|
132
|
+
|
|
127
133
|
export function recordFromTask(manifest: TeamRunManifest, task: TeamTaskState, runtime: CrewRuntimeKind): CrewAgentRecord {
|
|
128
134
|
return {
|
|
129
135
|
id: `${manifest.runId}:${task.id}`,
|
|
@@ -142,6 +148,8 @@ export function recordFromTask(manifest: TeamRunManifest, task: TeamTaskState, r
|
|
|
142
148
|
outputPath: agentOutputPath(manifest, task.id),
|
|
143
149
|
toolUses: task.agentProgress?.toolCount,
|
|
144
150
|
jsonEvents: task.jsonEvents,
|
|
151
|
+
model: modelFromTask(task),
|
|
152
|
+
usage: task.usage,
|
|
145
153
|
progress: task.agentProgress,
|
|
146
154
|
error: task.error,
|
|
147
155
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TeamTaskStatus } from "../state/contracts.ts";
|
|
2
|
+
import type { UsageState } from "../state/types.ts";
|
|
2
3
|
|
|
3
4
|
export type CrewRuntimeKind = "scaffold" | "child-process" | "live-session";
|
|
4
5
|
export type CrewAgentStatus = "queued" | "running" | "completed" | "failed" | "cancelled" | "stopped";
|
|
@@ -41,6 +42,8 @@ export interface CrewAgentRecord {
|
|
|
41
42
|
outputPath?: string;
|
|
42
43
|
toolUses?: number;
|
|
43
44
|
jsonEvents?: number;
|
|
45
|
+
model?: string;
|
|
46
|
+
usage?: UsageState;
|
|
44
47
|
progress?: CrewAgentProgress;
|
|
45
48
|
error?: string;
|
|
46
49
|
}
|
|
@@ -72,13 +72,14 @@ function textFromContent(content: unknown): string[] {
|
|
|
72
72
|
function extractText(value: unknown): string[] {
|
|
73
73
|
const obj = asRecord(value);
|
|
74
74
|
if (!obj) return [];
|
|
75
|
+
const message = asRecord(obj.message);
|
|
76
|
+
if (message?.role !== undefined && message.role !== "assistant") return [];
|
|
75
77
|
const text: string[] = [];
|
|
76
78
|
if (typeof obj.text === "string") text.push(obj.text);
|
|
77
79
|
if (typeof obj.output === "string") text.push(obj.output);
|
|
78
80
|
if (typeof obj.finalOutput === "string") text.push(obj.finalOutput);
|
|
79
81
|
if (typeof obj.final_output === "string") text.push(obj.final_output);
|
|
80
|
-
text.push(...textFromContent(obj.content));
|
|
81
|
-
const message = asRecord(obj.message);
|
|
82
|
+
if (!message) text.push(...textFromContent(obj.content));
|
|
82
83
|
if (message) text.push(...textFromContent(message.content));
|
|
83
84
|
return text.filter((entry) => entry.trim().length > 0);
|
|
84
85
|
}
|
|
@@ -56,7 +56,7 @@ export function evaluateCrewPolicy(input: PolicyEngineInput): PolicyDecision[] {
|
|
|
56
56
|
const maxRetries = input.limits?.maxRetriesPerTask ?? 0;
|
|
57
57
|
decisions.push(decision(retryCount < maxRetries ? "retry" : "escalate", "task_failed", task.error ? `Task failed: ${task.error}` : "Task failed.", task.id));
|
|
58
58
|
}
|
|
59
|
-
if (task.heartbeat && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
|
|
59
|
+
if ((task.status === "running" || task.status === "queued") && task.heartbeat && task.heartbeat.alive !== false && isWorkerHeartbeatStale(task.heartbeat, input.limits?.heartbeatStaleMs ?? 60_000, input.now)) {
|
|
60
60
|
decisions.push(decision("escalate", "worker_stale", "Worker heartbeat is stale.", task.id));
|
|
61
61
|
}
|
|
62
62
|
if (task.taskPacket?.verification) {
|
|
@@ -190,6 +190,17 @@ function shouldFlushProgressEvent(event: unknown): boolean {
|
|
|
190
190
|
return type === "tool_execution_start" || type === "tool_execution_end" || type === "message_end" || type === "tool_result_end";
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
function cleanResultText(text: string | undefined): string | undefined {
|
|
194
|
+
const trimmed = text?.trim();
|
|
195
|
+
if (!trimmed) return undefined;
|
|
196
|
+
const doneIndex = trimmed.lastIndexOf("\nDONE\n");
|
|
197
|
+
if (doneIndex >= 0) return trimmed.slice(doneIndex + 1).trim();
|
|
198
|
+
if (trimmed === "DONE" || trimmed.startsWith("DONE\n")) return trimmed;
|
|
199
|
+
const fencedPromptIndex = trimmed.lastIndexOf("</file>");
|
|
200
|
+
if (fencedPromptIndex >= 0 && fencedPromptIndex < trimmed.length - 7) return trimmed.slice(fencedPromptIndex + 7).trim() || trimmed;
|
|
201
|
+
return trimmed;
|
|
202
|
+
}
|
|
203
|
+
|
|
193
204
|
function progressEventSummary(task: TeamTaskState, event: unknown): Record<string, unknown> {
|
|
194
205
|
const type = asRecord(event)?.type;
|
|
195
206
|
return {
|
|
@@ -309,6 +320,10 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
309
320
|
for (let i = 0; i < attemptModels.length; i++) {
|
|
310
321
|
const model = attemptModels[i];
|
|
311
322
|
const attemptStartedAt = new Date();
|
|
323
|
+
const pendingAttempt: ModelAttemptSummary = { model: model ?? "default", success: false };
|
|
324
|
+
task = { ...task, modelAttempts: [...modelAttempts, pendingAttempt] };
|
|
325
|
+
tasks = updateTask(tasks, task);
|
|
326
|
+
upsertCrewAgent(manifest, recordFromTask(manifest, task, "child-process"));
|
|
312
327
|
const childResult = await runChildPi({
|
|
313
328
|
cwd: task.cwd,
|
|
314
329
|
task: prompt,
|
|
@@ -329,11 +344,13 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
329
344
|
exitCode = childResult.exitCode;
|
|
330
345
|
finalStdout = childResult.stdout;
|
|
331
346
|
finalStderr = childResult.stderr;
|
|
332
|
-
parsedOutput = parsePiJsonOutput(childResult.stdout);
|
|
347
|
+
parsedOutput = parsePiJsonOutput(fs.existsSync(transcriptPath) ? fs.readFileSync(transcriptPath, "utf-8") : childResult.stdout);
|
|
333
348
|
error = childResult.error || (childResult.exitCode && childResult.exitCode !== 0 ? childResult.stderr || `Child Pi exited with ${childResult.exitCode}` : undefined);
|
|
334
349
|
persistChildProgress({ type: "attempt_finished" }, true);
|
|
335
350
|
const attempt: ModelAttemptSummary = { model: model ?? "default", success: !error, exitCode, error };
|
|
336
351
|
modelAttempts.push(attempt);
|
|
352
|
+
task = { ...task, modelAttempts: [...modelAttempts] };
|
|
353
|
+
tasks = updateTask(tasks, task);
|
|
337
354
|
logs.push(`MODEL ATTEMPT ${i + 1}: ${attempt.model}`, `success=${attempt.success}`, `exitCode=${attempt.exitCode ?? "null"}`, attempt.error ? `error=${attempt.error}` : "", "");
|
|
338
355
|
if (!error) break;
|
|
339
356
|
const nextModel = attemptModels[i + 1];
|
|
@@ -343,7 +360,7 @@ export async function runTeamTask(input: TaskRunnerInput): Promise<{ manifest: T
|
|
|
343
360
|
resultArtifact = writeArtifact(manifest.artifactsRoot, {
|
|
344
361
|
kind: "result",
|
|
345
362
|
relativePath: `results/${task.id}.txt`,
|
|
346
|
-
content: parsedOutput?.finalText
|
|
363
|
+
content: cleanResultText(parsedOutput?.finalText) ?? cleanResultText(finalStdout) ?? cleanResultText(finalStderr) ?? "(no output)",
|
|
347
364
|
producer: task.id,
|
|
348
365
|
});
|
|
349
366
|
logArtifact = writeArtifact(manifest.artifactsRoot, {
|
package/src/ui/run-dashboard.ts
CHANGED
|
@@ -13,6 +13,7 @@ interface DashboardComponent {
|
|
|
13
13
|
type DashboardTheme = { fg?: (color: string, text: string) => string; bold?: (text: string) => string };
|
|
14
14
|
|
|
15
15
|
export interface RunDashboardOptions {
|
|
16
|
+
placement?: "center" | "right";
|
|
16
17
|
showModel?: boolean;
|
|
17
18
|
showTokens?: boolean;
|
|
18
19
|
showTools?: boolean;
|
|
@@ -126,11 +127,19 @@ function modelForTask(task: TeamTaskState | undefined): string | undefined {
|
|
|
126
127
|
return attempts.find((attempt) => attempt.success)?.model ?? attempts.at(-1)?.model;
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
function modelForAgent(agent: CrewAgentRecord, task: TeamTaskState | undefined): string | undefined {
|
|
131
|
+
return modelForTask(task) ?? agent.model;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function usageForAgent(agent: CrewAgentRecord, task: TeamTaskState | undefined): UsageState | undefined {
|
|
135
|
+
return task?.usage ?? agent.usage;
|
|
136
|
+
}
|
|
137
|
+
|
|
129
138
|
function agentPreviewLine(agent: CrewAgentRecord, task: TeamTaskState | undefined, options: RunDashboardOptions): string {
|
|
130
139
|
const stats = [
|
|
131
140
|
agent.progress?.activityState,
|
|
132
|
-
options.showModel !== false &&
|
|
133
|
-
options.showTokens !== false ? formatTokens(task
|
|
141
|
+
options.showModel !== false && modelForAgent(agent, task) ? `model=${modelForAgent(agent, task)}` : undefined,
|
|
142
|
+
options.showTokens !== false ? formatTokens(usageForAgent(agent, task)) ?? (agent.progress?.tokens !== undefined ? `tok=${agent.progress.tokens}` : undefined) : undefined,
|
|
134
143
|
options.showTools !== false && agent.progress?.currentTool ? `tool=${agent.progress.currentTool}` : undefined,
|
|
135
144
|
options.showTools !== false && agent.toolUses !== undefined ? `${agent.toolUses} tools` : undefined,
|
|
136
145
|
agent.progress?.turns !== undefined ? `${agent.progress.turns} turns` : undefined,
|
|
@@ -219,9 +228,10 @@ export class RunDashboard implements DashboardComponent {
|
|
|
219
228
|
const innerWidth = Math.max(20, width - 4);
|
|
220
229
|
const borderWidth = Math.min(innerWidth, Math.max(0, width - 2));
|
|
221
230
|
const border = (text: string) => fg("border", text);
|
|
231
|
+
const title = this.options.placement === "right" ? "pi-crew right sidebar" : "pi-crew dashboard";
|
|
222
232
|
const lines = [
|
|
223
233
|
border(`╭${"─".repeat(borderWidth)}╮`),
|
|
224
|
-
`│ ${padVisible(truncate(`${fg("accent", "
|
|
234
|
+
`│ ${padVisible(truncate(`${fg("accent", "▐")} ${bold(title)} ${this.options.placement === "right" ? fg("dim", "anchored top-right") : ""}`, innerWidth - 1), innerWidth - 1)}│`,
|
|
225
235
|
`│ ${padVisible(truncate(fg("dim", "↑/↓/j/k select • r reload • p progress • s/u/a/i actions • d agents • e/v/o viewers • q close"), innerWidth - 1), innerWidth - 1)}│`,
|
|
226
236
|
`│ ${padVisible(truncate(`Runs: ${this.runs.length} • ${countByStatus(this.runs)}`, innerWidth - 1), innerWidth - 1)}│`,
|
|
227
237
|
border(`├${"─".repeat(borderWidth)}┤`),
|