pi-crew 0.9.10 → 0.9.12
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 +90 -0
- package/package.json +1 -1
- package/src/config/role-tools.ts +39 -6
- package/src/extension/crew-shortcuts.ts +29 -2
- package/src/extension/registration/commands.ts +61 -34
- package/src/runtime/async-runner.ts +70 -74
- package/src/runtime/background-runner.ts +13 -2
- package/src/runtime/process-status.ts +7 -2
- package/src/runtime/role-permission.ts +5 -21
- package/src/runtime/task-runner/prompt-builder.ts +1 -0
- package/src/state/artifact-store.ts +22 -2
- package/src/ui/crew-footer.ts +3 -3
- package/src/ui/crew-select-list.ts +1 -1
- package/src/ui/dashboard-panes/agents-pane.ts +26 -4
- package/src/ui/dashboard-panes/cancellation-pane.ts +23 -0
- package/src/ui/keybinding-map.ts +7 -3
- package/src/ui/live-conversation-overlay.ts +2 -2
- package/src/ui/live-run-sidebar.ts +18 -10
- package/src/ui/overlays/help-overlay.ts +166 -0
- package/src/ui/run-dashboard.ts +210 -70
- package/src/ui/status-colors.ts +45 -0
- package/src/ui/widget/index.ts +46 -3
- package/src/ui/widget/widget-formatters.ts +22 -7
- package/src/ui/widget/widget-renderer.ts +31 -27
- package/src/utils/redaction.ts +49 -31
- package/src/utils/visual.ts +3 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { CrewTheme } from "../theme-adapter.ts";
|
|
8
|
-
import { iconForStatus } from "../status-colors.ts";
|
|
8
|
+
import { iconForStatus, colorizeStatusGlyphs } from "../status-colors.ts";
|
|
9
9
|
import { truncate } from "../../utils/visual.ts";
|
|
10
10
|
import { Box, Text } from "../layout-primitives.ts";
|
|
11
11
|
import { listLiveAgents } from "../../runtime/live-agent-manager.ts";
|
|
@@ -89,19 +89,21 @@ export function buildWidgetLines(cwd: string, frame = 0, maxLines = 8, providedR
|
|
|
89
89
|
|
|
90
90
|
const liveForRun = listLiveAgents().filter((a) => a.runId === run.runId);
|
|
91
91
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
92
|
+
// L-4: prioritize RUNNING > QUEUED > WAITING within the visible window so the
|
|
93
|
+
// most relevant live workers are always shown. Finished rows fill only the
|
|
94
|
+
// leftover budget and never steal slots from running agents.
|
|
95
|
+
const ACTIVE_PRIORITY: Record<string, number> = { running: 0, queued: 1, waiting: 2 };
|
|
96
|
+
const prioritizedActive = [...activeAgents].sort(
|
|
97
|
+
(a, b) => (ACTIVE_PRIORITY[a.status] ?? 9) - (ACTIVE_PRIORITY[b.status] ?? 9),
|
|
98
|
+
);
|
|
99
|
+
// Finished rows only appear in slots not used by active agents (max 2). When
|
|
100
|
+
// there are >= MAX_AGENTS_DISPLAY live workers, finished rows are suppressed
|
|
101
|
+
// entirely so they cannot push a live agent's activity line off-screen.
|
|
102
|
+
const finishedSlots = Math.max(0, Math.min(2, MAX_AGENTS_DISPLAY - activeAgents.length));
|
|
103
|
+
|
|
104
|
+
const visibleAgents = prioritizedActive.slice(0, MAX_AGENTS_DISPLAY);
|
|
103
105
|
for (const [index, agent] of visibleAgents.entries()) {
|
|
104
|
-
const last = index === visibleAgents.length - 1 && activeAgents.length <= MAX_AGENTS_DISPLAY;
|
|
106
|
+
const last = index === visibleAgents.length - 1 && activeAgents.length <= MAX_AGENTS_DISPLAY && finishedSlots === 0;
|
|
105
107
|
const branch = last ? "└─" : "├─";
|
|
106
108
|
const agentGlyph = iconForStatus(agent.status, { runningGlyph });
|
|
107
109
|
const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
|
|
@@ -118,6 +120,18 @@ export function buildWidgetLines(cwd: string, frame = 0, maxLines = 8, providedR
|
|
|
118
120
|
lines.push(truncate(`│ └─ … +${activeAgents.length - MAX_AGENTS_DISPLAY} more agents`, width));
|
|
119
121
|
}
|
|
120
122
|
|
|
123
|
+
for (const [index, agent] of finishedAgents.slice(0, finishedSlots).entries()) {
|
|
124
|
+
const liveHandle = liveForRun.find((h) => h.taskId === agent.taskId);
|
|
125
|
+
const name = liveHandle?.agent ?? agent.agent;
|
|
126
|
+
const icon = agent.status === "completed" ? "✓" : agent.status === "failed" ? "✗" : agent.status === "needs_attention" ? "⚠" : "▪";
|
|
127
|
+
const stats = agentStats(agent, liveHandle);
|
|
128
|
+
const desc = truncate(liveHandle?.description ?? agent.role ?? "", TASK_DESC_MAX);
|
|
129
|
+
const isLastFinished = index === Math.min(finishedAgents.length, finishedSlots) - 1;
|
|
130
|
+
const branch = isLastFinished ? "└─" : "├─";
|
|
131
|
+
const _finished = truncate(`│ ${branch} ${icon} ${name} · ${desc}${stats ? ` · ${stats}` : ""}`, width);
|
|
132
|
+
lines.push(_finished);
|
|
133
|
+
}
|
|
134
|
+
|
|
121
135
|
if (lines.length >= maxLines) break;
|
|
122
136
|
}
|
|
123
137
|
|
|
@@ -126,25 +140,15 @@ export function buildWidgetLines(cwd: string, frame = 0, maxLines = 8, providedR
|
|
|
126
140
|
|
|
127
141
|
// ── Colorization ──────────────────────────────────────────────────────
|
|
128
142
|
|
|
129
|
-
function statusGlyphColor(icon: string): Parameters<CrewTheme["fg"]>[0] {
|
|
130
|
-
const mapping: Record<string, Parameters<CrewTheme["fg"]>[0]> = {
|
|
131
|
-
"✓": "success",
|
|
132
|
-
"✗": "error",
|
|
133
|
-
"■": "warning",
|
|
134
|
-
"⏸": "warning",
|
|
135
|
-
"◦": "dim",
|
|
136
|
-
"·": "dim",
|
|
137
|
-
"▶": "accent",
|
|
138
|
-
};
|
|
139
|
-
return mapping[icon] ?? "accent";
|
|
140
|
-
}
|
|
141
|
-
|
|
142
143
|
export function colorWidgetLine(line: string, index: number, theme: CrewTheme): string {
|
|
143
144
|
let result = line;
|
|
144
145
|
if (index === 0) {
|
|
145
146
|
result = result.replace("Crew agents", theme.bold(theme.fg("accent", "Crew agents")));
|
|
146
147
|
}
|
|
147
|
-
|
|
148
|
+
// Shared glyph colorizer covers ALL status glyphs — including ⏳ (waiting),
|
|
149
|
+
// ⚠ (needs_attention), and the braille spinner range ⠁-⣿ (running) — which the
|
|
150
|
+
// previous local statusGlyphColor map + regex omitted (F-1, V-3).
|
|
151
|
+
result = colorizeStatusGlyphs(result, theme);
|
|
148
152
|
if (index === 0) {
|
|
149
153
|
result = theme.fg("accent", result);
|
|
150
154
|
}
|
package/src/utils/redaction.ts
CHANGED
|
@@ -97,34 +97,54 @@ export function isSecretKey(keyName: string): boolean {
|
|
|
97
97
|
return false;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
//
|
|
100
|
+
// Boundary chars that may precede an "authorization:" or "Bearer " keyword.
|
|
101
|
+
// Includes '-' so prefixed headers (Proxy-Authorization, X-Authorization) and
|
|
102
|
+
// '\t' so tab-indented headers are recognized. See security review L5.
|
|
103
|
+
const AUTH_HEADER_BOUNDARY_CHARS = new Set([" ", ",", "{", "[", "\"", "\r", "\n", "-", "\t"]);
|
|
104
|
+
function isAuthHeaderBoundary(ch: string | undefined): boolean {
|
|
105
|
+
return ch !== undefined && AUTH_HEADER_BOUNDARY_CHARS.has(ch);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Linear-time Authorization header redaction.
|
|
109
|
+
// L3 fix: scan ALL occurrences (previously first-only via indexOf, so a second
|
|
110
|
+
// "authorization:" on a later line leaked). L5 fix: boundary set includes '-'
|
|
111
|
+
// and '\t' so Proxy-Authorization / X-Authorization / tab-indented headers are
|
|
112
|
+
// redacted. Bearer values are left for redactBearerTokens.
|
|
101
113
|
export function redactAuthHeader(line: string): string {
|
|
102
114
|
const lower = line.toLowerCase();
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
return
|
|
115
|
+
let result = "";
|
|
116
|
+
let i = 0; // emit cursor into the original `line`
|
|
117
|
+
let searchFrom = 0; // cursor for the next indexOf scan
|
|
118
|
+
for (;;) {
|
|
119
|
+
const authIdx = lower.indexOf("authorization:", searchFrom);
|
|
120
|
+
if (authIdx === -1) {
|
|
121
|
+
result += line.substring(i);
|
|
122
|
+
return result;
|
|
111
123
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
end
|
|
124
|
+
// Emit the unchanged span up to this occurrence.
|
|
125
|
+
result += line.substring(i, authIdx);
|
|
126
|
+
const isBoundary = authIdx === 0 || isAuthHeaderBoundary(line[authIdx - 1]);
|
|
127
|
+
const afterAuth = lower.substring(authIdx + 14).trimStart();
|
|
128
|
+
if (isBoundary && !afterAuth.startsWith("bearer ")) {
|
|
129
|
+
// Regular Authorization header — blank the credential value (the rest
|
|
130
|
+
// of the line is the credential). Appending only a marker would leave
|
|
131
|
+
// the secret bytes visible; replace them with "***". Bearer values are
|
|
132
|
+
// left intact here for redactBearerTokens. See security review L3/L5.
|
|
133
|
+
let end = authIdx + 14;
|
|
134
|
+
while (end < line.length && line[end] !== "\r" && line[end] !== "\n") {
|
|
135
|
+
end++;
|
|
136
|
+
}
|
|
137
|
+
result += line.substring(authIdx, authIdx + 14) + " ***";
|
|
138
|
+
i = end;
|
|
139
|
+
searchFrom = end; // entire line consumed; resume after the line break
|
|
140
|
+
} else {
|
|
141
|
+
// Bearer token (handled by redactBearerTokens) OR not a boundary —
|
|
142
|
+
// keep the "authorization:" literal and continue scanning.
|
|
143
|
+
result += line.substring(authIdx, authIdx + 14);
|
|
144
|
+
i = authIdx + 14;
|
|
145
|
+
searchFrom = authIdx + 14;
|
|
122
146
|
}
|
|
123
|
-
return line.substring(0, end) + " ***" + (end < line.length ? line.substring(end) : "");
|
|
124
147
|
}
|
|
125
|
-
|
|
126
|
-
// It's a Bearer token format - don't redact here, let redactBearerTokens handle it
|
|
127
|
-
return line;
|
|
128
148
|
}
|
|
129
149
|
|
|
130
150
|
// Linear-time Bearer token redaction
|
|
@@ -135,14 +155,12 @@ export function redactBearerTokens(line: string): string {
|
|
|
135
155
|
|
|
136
156
|
while (i < line.length) {
|
|
137
157
|
if (upper.startsWith("BEARER ", i)) {
|
|
138
|
-
// Check word boundary:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
158
|
+
// Check word boundary: start-of-string or a boundary char. Includes '-'
|
|
159
|
+
// and '\t' (L5) so "Proxy-Authorization: Bearer ..." is redacted.
|
|
160
|
+
if (i > 0 && !isAuthHeaderBoundary(line[i - 1])) {
|
|
161
|
+
result.push(line[i]);
|
|
162
|
+
i++;
|
|
163
|
+
continue;
|
|
146
164
|
}
|
|
147
165
|
|
|
148
166
|
// Found "Bearer " - now find the token
|
package/src/utils/visual.ts
CHANGED
|
@@ -39,8 +39,10 @@ const WIDE_RANGES: Array<[number, number]> = [
|
|
|
39
39
|
[0x1FA00, 0x1FAFF], // Symbols Extended-A
|
|
40
40
|
[0x1F000, 0x1F02F], // Mahjong, Dominos
|
|
41
41
|
[0xFE00, 0xFE0F], // Variation Selectors (emoji presentation)
|
|
42
|
-
[0x200D, 0x200D], // Zero Width Joiner (creates compound emoji)
|
|
43
42
|
];
|
|
43
|
+
// NOTE: U+200D (Zero Width Joiner, ZWJ) is intentionally NOT listed as wide.
|
|
44
|
+
// ZWJ has zero advance width by Unicode definition; miscounting it as 2 caused
|
|
45
|
+
// slight over-truncation/over-padding of compound-emoji goals (T-1).
|
|
44
46
|
|
|
45
47
|
function isWideCodePoint(code: number): boolean {
|
|
46
48
|
for (const [lo, hi] of WIDE_RANGES) {
|