pi-crew 0.6.3 → 0.6.4
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 +34 -0
- package/README.md +12 -1
- package/docs/ui-optimization-plan.md +447 -0
- package/package.json +1 -1
- package/src/extension/register.ts +19 -1
- package/src/extension/registration/brief-tool-overrides.ts +400 -0
- package/src/extension/registration/commands.ts +25 -0
- package/src/extension/registration/subagent-tools.ts +8 -3
- package/src/extension/registration/team-tool.ts +18 -11
- package/src/extension/team-tool/run.ts +4 -3
- package/src/extension/team-tool-types.ts +2 -0
- package/src/state/session-state-map.ts +51 -0
- package/src/ui/powerbar-publisher.ts +1 -1
- package/src/ui/status-colors.ts +5 -1
- package/src/ui/theme-adapter.ts +80 -1
- package/src/ui/tool-progress-formatter.ts +9 -5
- package/src/ui/tool-render.ts +4 -0
- package/src/ui/tool-renderers/brief-mode.ts +207 -0
- package/src/ui/tool-renderers/index.ts +627 -0
- package/src/ui/widget/index.ts +224 -0
- package/src/ui/widget/widget-formatters.ts +148 -0
- package/src/ui/widget/widget-model.ts +90 -0
- package/src/ui/widget/widget-renderer.ts +130 -0
- package/src/ui/widget/widget-types.ts +37 -0
- package/src/utils/guards.ts +110 -0
package/src/ui/theme-adapter.ts
CHANGED
|
@@ -9,9 +9,22 @@ export type CrewThemeColor =
|
|
|
9
9
|
| "muted"
|
|
10
10
|
| "dim"
|
|
11
11
|
| "text"
|
|
12
|
+
| "thinkingText"
|
|
13
|
+
// Tool rendering
|
|
14
|
+
| "toolTitle"
|
|
15
|
+
| "toolOutput"
|
|
12
16
|
| "toolDiffAdded"
|
|
13
17
|
| "toolDiffRemoved"
|
|
14
18
|
| "toolDiffContext"
|
|
19
|
+
// Markdown
|
|
20
|
+
| "mdHeading"
|
|
21
|
+
| "mdLink"
|
|
22
|
+
| "mdCode"
|
|
23
|
+
| "mdCodeBlock"
|
|
24
|
+
| "mdQuote"
|
|
25
|
+
| "mdHr"
|
|
26
|
+
| "mdListBullet"
|
|
27
|
+
// Syntax highlighting
|
|
15
28
|
| "syntaxKeyword"
|
|
16
29
|
| "syntaxString"
|
|
17
30
|
| "syntaxNumber"
|
|
@@ -21,15 +34,81 @@ export type CrewThemeColor =
|
|
|
21
34
|
| "syntaxType"
|
|
22
35
|
| "syntaxOperator"
|
|
23
36
|
| "syntaxPunctuation"
|
|
24
|
-
|
|
37
|
+
// Message display
|
|
38
|
+
| "userMessageText"
|
|
39
|
+
| "customMessageLabel"
|
|
40
|
+
// Thinking gradient (6 levels, low→high intensity)
|
|
41
|
+
| "thinkingOff"
|
|
42
|
+
| "thinkingMinimal"
|
|
43
|
+
| "thinkingLow"
|
|
44
|
+
| "thinkingMedium"
|
|
45
|
+
| "thinkingHigh"
|
|
46
|
+
| "thinkingXhigh"
|
|
47
|
+
// Special
|
|
48
|
+
| "bashMode";
|
|
25
49
|
|
|
26
50
|
export type CrewThemeBg =
|
|
27
51
|
| "selectedBg"
|
|
28
52
|
| "userMessageBg"
|
|
53
|
+
| "customMessageBg"
|
|
29
54
|
| "toolPendingBg"
|
|
30
55
|
| "toolSuccessBg"
|
|
31
56
|
| "toolErrorBg";
|
|
32
57
|
|
|
58
|
+
/** ANSI fallback values for theme color slots when the active theme doesn't define them. */
|
|
59
|
+
export const THEME_COLOR_FALLBACKS: Record<CrewThemeColor, string> = {
|
|
60
|
+
accent: "\x1b[36m",
|
|
61
|
+
border: "\x1b[38;5;240m",
|
|
62
|
+
borderAccent: "\x1b[35m",
|
|
63
|
+
borderMuted: "\x1b[38;5;236m",
|
|
64
|
+
success: "\x1b[32m",
|
|
65
|
+
error: "\x1b[31m",
|
|
66
|
+
warning: "\x1b[33m",
|
|
67
|
+
muted: "\x1b[38;5;245m",
|
|
68
|
+
dim: "\x1b[38;5;240m",
|
|
69
|
+
text: "\x1b[39m",
|
|
70
|
+
thinkingText: "\x1b[38;5;245m",
|
|
71
|
+
toolTitle: "\x1b[36m",
|
|
72
|
+
toolOutput: "\x1b[38;5;245m",
|
|
73
|
+
toolDiffAdded: "\x1b[32m",
|
|
74
|
+
toolDiffRemoved: "\x1b[31m",
|
|
75
|
+
toolDiffContext: "\x1b[38;5;245m",
|
|
76
|
+
mdHeading: "\x1b[33m",
|
|
77
|
+
mdLink: "\x1b[35m",
|
|
78
|
+
mdCode: "\x1b[32m",
|
|
79
|
+
mdCodeBlock: "\x1b[39m",
|
|
80
|
+
mdQuote: "\x1b[38;5;245m",
|
|
81
|
+
mdHr: "\x1b[38;5;240m",
|
|
82
|
+
mdListBullet: "\x1b[36m",
|
|
83
|
+
syntaxKeyword: "\x1b[35m",
|
|
84
|
+
syntaxString: "\x1b[32m",
|
|
85
|
+
syntaxNumber: "\x1b[33m",
|
|
86
|
+
syntaxComment: "\x1b[38;5;245m",
|
|
87
|
+
syntaxFunction: "\x1b[36m",
|
|
88
|
+
syntaxVariable: "\x1b[39m",
|
|
89
|
+
syntaxType: "\x1b[35m",
|
|
90
|
+
syntaxOperator: "\x1b[35m",
|
|
91
|
+
syntaxPunctuation: "\x1b[35m",
|
|
92
|
+
userMessageText: "\x1b[39m",
|
|
93
|
+
customMessageLabel: "\x1b[35m",
|
|
94
|
+
thinkingOff: "\x1b[38;5;236m",
|
|
95
|
+
thinkingMinimal: "\x1b[38;5;245m",
|
|
96
|
+
thinkingLow: "\x1b[35m",
|
|
97
|
+
thinkingMedium: "\x1b[35m",
|
|
98
|
+
thinkingHigh: "\x1b[36m",
|
|
99
|
+
thinkingXhigh: "\x1b[35m",
|
|
100
|
+
bashMode: "\x1b[32m",
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Map a thinking intensity level (0–5) to a theme color slot. */
|
|
104
|
+
export function thinkingColorForLevel(level: number): CrewThemeColor {
|
|
105
|
+
const slots: CrewThemeColor[] = [
|
|
106
|
+
"thinkingOff", "thinkingMinimal", "thinkingLow",
|
|
107
|
+
"thinkingMedium", "thinkingHigh", "thinkingXhigh",
|
|
108
|
+
];
|
|
109
|
+
return slots[Math.min(Math.max(level, 0), 5)] ?? "thinkingOff";
|
|
110
|
+
}
|
|
111
|
+
|
|
33
112
|
export interface CrewTheme {
|
|
34
113
|
fg(color: CrewThemeColor, text: string): string;
|
|
35
114
|
bg?(color: CrewThemeBg, text: string): string;
|
|
@@ -48,12 +48,14 @@ function trimLine(value: string): string {
|
|
|
48
48
|
|
|
49
49
|
function taskCounts(tasks: TeamTaskState[] | undefined): string | undefined {
|
|
50
50
|
if (!tasks || tasks.length === 0) return undefined;
|
|
51
|
+
const total = tasks.length;
|
|
52
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
51
53
|
const buckets = new Map<string, number>();
|
|
52
54
|
for (const task of tasks) buckets.set(task.status, (buckets.get(task.status) ?? 0) + 1);
|
|
53
55
|
const summary = [...buckets.entries()]
|
|
54
56
|
.map(([status, count]) => `${status}=${count}`)
|
|
55
57
|
.join(" ");
|
|
56
|
-
return `tasks ${summary}`;
|
|
58
|
+
return `tasks ${completed}/${total} done ${summary}`;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
/**
|
|
@@ -66,6 +68,9 @@ export function formatCompactToolProgress(input: ToolProgressInput): string {
|
|
|
66
68
|
const head = input.agentId ? `agent=${input.agentId}` : "agent";
|
|
67
69
|
const lines: string[] = [`${head} status=${input.status} elapsed=${elapsedSec}s`];
|
|
68
70
|
|
|
71
|
+
const counts = taskCounts(input.tasks);
|
|
72
|
+
if (counts) lines.push(` ${counts}`);
|
|
73
|
+
|
|
69
74
|
const active = pickActiveAgent(input.agents);
|
|
70
75
|
if (active) {
|
|
71
76
|
const turns = active.progress?.turns ?? 0;
|
|
@@ -77,12 +82,11 @@ export function formatCompactToolProgress(input: ToolProgressInput): string {
|
|
|
77
82
|
}
|
|
78
83
|
const recent = active.progress?.recentOutput?.at(-1);
|
|
79
84
|
if (recent && recent.trim()) lines.push(` ${trimLine(recent)}`);
|
|
80
|
-
} else if (input.runId) {
|
|
81
|
-
|
|
82
|
-
lines.push(counts ? ` run=${input.runId} ${counts}` : ` run=${input.runId} (starting)`);
|
|
85
|
+
} else if (input.runId && !counts) {
|
|
86
|
+
lines.push(` run=${input.runId} (starting)`);
|
|
83
87
|
} else if (input.error) {
|
|
84
88
|
lines.push(` error: ${trimLine(input.error)}`);
|
|
85
|
-
} else {
|
|
89
|
+
} else if (!counts) {
|
|
86
90
|
lines.push(" waiting for run to start");
|
|
87
91
|
}
|
|
88
92
|
return lines.join("\n");
|
package/src/ui/tool-render.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @deprecated Use tool-renderers/index.ts (teamToolRenderer/agentToolRenderer) instead.
|
|
3
|
+
* This file only exports shared utility functions (truncLine, formatTokens, formatDuration).
|
|
4
|
+
* The render functions below are kept for backward-compatible tests only.
|
|
5
|
+
*
|
|
2
6
|
* Shared rendering for pi-crew's tool TUI display.
|
|
3
7
|
* Ports logic from pi-subagent4 adapted for pi-crew's data model.
|
|
4
8
|
* Uses @earendil-works/pi-tui Components (Container, Text, Spacer) directly.
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brief mode for tool result display.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by @ayulab/pi-brief — shows one-line summaries instead of
|
|
5
|
+
* full output. Toggled via /crew-brief on|off, state persists across
|
|
6
|
+
* session reloads via pi.appendEntry().
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CrewTheme } from "../theme-adapter.ts";
|
|
10
|
+
import { formatTokens, formatDuration, truncLine } from "../tool-render.ts";
|
|
11
|
+
import type { CrewAgentRecord } from "../../runtime/crew-agent-runtime.ts";
|
|
12
|
+
|
|
13
|
+
// ── State ──────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const BRIEF_ENTRY_TYPE = "pi-crew.brief-state";
|
|
16
|
+
let briefEnabled = false;
|
|
17
|
+
|
|
18
|
+
export function isBrief(): boolean {
|
|
19
|
+
return briefEnabled;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function setBrief(on: boolean): void {
|
|
23
|
+
briefEnabled = on;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Entry type for persisting brief state across session reloads. */
|
|
27
|
+
export interface BriefStateEntry {
|
|
28
|
+
readonly type: "custom";
|
|
29
|
+
readonly customType: typeof BRIEF_ENTRY_TYPE;
|
|
30
|
+
readonly data: { enabled: boolean };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function makeBriefEntry(enabled: boolean): BriefStateEntry {
|
|
34
|
+
return { type: "custom", customType: BRIEF_ENTRY_TYPE, data: { enabled } };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function restoreBriefState(entries: Iterable<unknown>): void {
|
|
38
|
+
for (const entry of entries) {
|
|
39
|
+
if (
|
|
40
|
+
typeof entry === "object" && entry !== null &&
|
|
41
|
+
"type" in entry && (entry as Record<string, unknown>).type === "custom" &&
|
|
42
|
+
"customType" in entry && (entry as Record<string, unknown>).customType === BRIEF_ENTRY_TYPE &&
|
|
43
|
+
"data" in entry
|
|
44
|
+
) {
|
|
45
|
+
const data = (entry as Record<string, unknown>).data;
|
|
46
|
+
if (typeof data === "object" && data !== null && "enabled" in data) {
|
|
47
|
+
briefEnabled = !!(data as Record<string, unknown>).enabled;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { BRIEF_ENTRY_TYPE };
|
|
54
|
+
|
|
55
|
+
// ── Brief renderers ────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/** Brief summary for a single tool result. */
|
|
58
|
+
export function briefToolResult(toolName: string, result: { content?: unknown[] }, theme: CrewTheme): string {
|
|
59
|
+
const text = extractText(result?.content);
|
|
60
|
+
switch (toolName) {
|
|
61
|
+
case "read": return briefRead(text, theme);
|
|
62
|
+
case "bash": return briefBash(text, theme);
|
|
63
|
+
case "edit": return briefEdit(text, theme);
|
|
64
|
+
case "write": return briefWrite(text, theme);
|
|
65
|
+
case "find": return briefFind(text, theme);
|
|
66
|
+
case "grep": return briefGrep(text, theme);
|
|
67
|
+
case "ls": return briefLs(text, theme);
|
|
68
|
+
case "team": return briefTeam(result, theme);
|
|
69
|
+
case "agent": return briefAgent(result, theme);
|
|
70
|
+
default: return briefDefault(text, theme);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function briefRead(text: string, theme: CrewTheme): string {
|
|
75
|
+
if (!text) return theme.fg("dim", "→ empty");
|
|
76
|
+
const count = text.trim().split("\n").filter(Boolean).length;
|
|
77
|
+
return theme.fg("muted", `→ ${count} lines`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function briefBash(text: string, theme: CrewTheme): string {
|
|
81
|
+
if (!text?.trim()) return theme.fg("dim", "→ done");
|
|
82
|
+
const lines = text.trim().split("\n");
|
|
83
|
+
if (lines.length === 1 && lines[0]!.length < 40) {
|
|
84
|
+
return theme.fg("muted", `→ ${lines[0]}`);
|
|
85
|
+
}
|
|
86
|
+
return theme.fg("muted", `→ ${lines.length} lines`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function briefEdit(text: string, theme: CrewTheme): string {
|
|
90
|
+
if (!text) return theme.fg("dim", "→ edited");
|
|
91
|
+
if (text.includes("Error") || text.includes("error")) {
|
|
92
|
+
return theme.fg("error", "→ failed");
|
|
93
|
+
}
|
|
94
|
+
const added = (text.match(/^\+ /gm) ?? []).length;
|
|
95
|
+
const removed = (text.match(/^- /gm) ?? []).length;
|
|
96
|
+
if (added === 0 && removed === 0) {
|
|
97
|
+
return theme.fg("success", "→ edited");
|
|
98
|
+
}
|
|
99
|
+
return theme.fg("success", "→ edited ") +
|
|
100
|
+
theme.fg("toolDiffAdded", `+${added} `) +
|
|
101
|
+
theme.fg("toolDiffRemoved", `-${removed}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function briefWrite(text: string, theme: CrewTheme): string {
|
|
105
|
+
if (text) return theme.fg("error", `→ ${text}`);
|
|
106
|
+
return theme.fg("success", "→ written");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function briefFind(text: string, theme: CrewTheme): string {
|
|
110
|
+
if (!text) return theme.fg("dim", "→ none");
|
|
111
|
+
const count = text.trim().split("\n").filter(Boolean).length;
|
|
112
|
+
return theme.fg("muted", `→ ${count} files`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function briefGrep(text: string, theme: CrewTheme): string {
|
|
116
|
+
if (!text) return theme.fg("dim", "→ none");
|
|
117
|
+
const count = text.trim().split("\n").filter(Boolean).length;
|
|
118
|
+
return theme.fg("muted", `→ ${count} matches`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function briefLs(text: string, theme: CrewTheme): string {
|
|
122
|
+
if (!text) return theme.fg("dim", "→ empty");
|
|
123
|
+
const count = text.trim().split("\n").filter(Boolean).length;
|
|
124
|
+
return theme.fg("muted", `→ ${count} entries`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function briefTeam(result: { content?: unknown[] }, theme: CrewTheme): string {
|
|
128
|
+
// Try to extract structured details
|
|
129
|
+
const details = (result as Record<string, unknown>).details ?? result;
|
|
130
|
+
const d = typeof details === "object" && details !== null ? details as Record<string, unknown> : {};
|
|
131
|
+
const status = typeof d.status === "string" ? d.status : "";
|
|
132
|
+
const runId = typeof d.runId === "string" ? d.runId : "";
|
|
133
|
+
const icon = status === "completed" ? theme.fg("success", "✓")
|
|
134
|
+
: status === "failed" ? theme.fg("error", "✗")
|
|
135
|
+
: status === "running" ? theme.fg("warning", "⟳")
|
|
136
|
+
: theme.fg("dim", "○");
|
|
137
|
+
|
|
138
|
+
// Agent records summary
|
|
139
|
+
const records = (d.agentRecords ?? d.results) as CrewAgentRecord[] | undefined;
|
|
140
|
+
if (records?.length) {
|
|
141
|
+
const completed = records.filter((r) => r.status === "completed").length;
|
|
142
|
+
const total = records.length;
|
|
143
|
+
const duration = computeTotalDuration(records);
|
|
144
|
+
const tokens = computeTotalTokens(records);
|
|
145
|
+
return `${icon} ${completed}/${total} tasks · ${formatDuration(duration)} · ${formatTokens(tokens)} tok`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Fallback: compact status line
|
|
149
|
+
const parts: string[] = [];
|
|
150
|
+
if (status) parts.push(status);
|
|
151
|
+
if (runId) parts.push(runId.slice(-8));
|
|
152
|
+
return `${icon} ${parts.join(" · ") || "done"}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function briefAgent(result: { content?: unknown[] }, theme: CrewTheme): string {
|
|
156
|
+
const d = (result as Record<string, unknown>).details ?? result;
|
|
157
|
+
const data = typeof d === "object" && d !== null ? d as Record<string, unknown> : {};
|
|
158
|
+
const status = typeof data.status === "string" ? data.status : "";
|
|
159
|
+
const agentId = typeof data.agentId === "string" ? data.agentId : "agent";
|
|
160
|
+
const icon = status === "completed" ? theme.fg("success", "\u2713")
|
|
161
|
+
: status === "failed" ? theme.fg("error", "\u2717")
|
|
162
|
+
: theme.fg("dim", "\u25CB");
|
|
163
|
+
// P3: Show duration and tokens when available
|
|
164
|
+
const parts: string[] = [agentId];
|
|
165
|
+
if (data.durationMs) parts.push(formatDuration(data.durationMs as number));
|
|
166
|
+
if (data.totalTokens) parts.push(`${formatTokens(data.totalTokens as number)} tok`);
|
|
167
|
+
return `${icon} ${parts.join(" \u00B7 ")}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function briefDefault(text: string, theme: CrewTheme): string {
|
|
171
|
+
if (!text) return theme.fg("dim", "→ done");
|
|
172
|
+
const first = text.split("\n")[0] ?? "";
|
|
173
|
+
return theme.fg("muted", `→ ${truncLine(first, 60)}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── Helpers ────────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
function extractText(content: unknown[] | undefined): string {
|
|
179
|
+
if (!content) return "";
|
|
180
|
+
if (!Array.isArray(content)) return String(content);
|
|
181
|
+
return content
|
|
182
|
+
.filter((c): c is Record<string, unknown> => typeof c === "object" && c !== null && (c as Record<string, unknown>).type === "text")
|
|
183
|
+
.map((c) => String((c as Record<string, unknown>).text ?? ""))
|
|
184
|
+
.join("\n");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function computeTotalDuration(records: CrewAgentRecord[]): number {
|
|
188
|
+
let total = 0;
|
|
189
|
+
for (const r of records) {
|
|
190
|
+
if (r.startedAt) {
|
|
191
|
+
const start = new Date(r.startedAt).getTime();
|
|
192
|
+
const end = r.completedAt ? new Date(r.completedAt).getTime() : Date.now();
|
|
193
|
+
if (Number.isFinite(start) && Number.isFinite(end)) total += Math.max(0, end - start);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return total;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function computeTotalTokens(records: CrewAgentRecord[]): number {
|
|
200
|
+
let total = 0;
|
|
201
|
+
for (const r of records) {
|
|
202
|
+
if (r.usage) {
|
|
203
|
+
total += (r.usage.input ?? 0) + (r.usage.output ?? 0) + (r.usage.cacheWrite ?? 0);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return total;
|
|
207
|
+
}
|