@ferris1225/pi-subagents 0.3.0 → 0.5.0
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/LICENSE +21 -21
- package/README-zh.md +153 -134
- package/README.md +167 -143
- package/agents/explore.md +42 -42
- package/agents/plan.md +41 -41
- package/agents/reviewer.md +45 -45
- package/agents/worker.md +44 -44
- package/package.json +54 -54
- package/src/agents.ts +157 -157
- package/src/config.ts +168 -155
- package/src/index.ts +437 -417
- package/src/models.ts +69 -0
- package/src/monitor.ts +275 -238
- package/src/prompt.ts +58 -57
- package/src/setup.ts +264 -222
- package/src/spawn.ts +473 -361
- package/src/ui.ts +231 -231
package/src/models.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model availability helpers shared by setup and sub-agent execution.
|
|
3
|
+
*
|
|
4
|
+
* A configured override can outlive a provider login, a scoped-model change, or
|
|
5
|
+
* a model rename. Keep the main session usable by replacing such overrides with
|
|
6
|
+
* the model currently selected in the main window (or the first available model)
|
|
7
|
+
* and let callers persist the repaired mapping.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
|
|
12
|
+
export type ModelContext = Pick<ExtensionContext, "model" | "scopedModels" | "modelRegistry">;
|
|
13
|
+
|
|
14
|
+
export interface ModelOverrideRepair {
|
|
15
|
+
agentModels: Record<string, string>;
|
|
16
|
+
changed: boolean;
|
|
17
|
+
replaced: number;
|
|
18
|
+
removed: number;
|
|
19
|
+
fallbackRef?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function modelRef(model: { provider: string; id: string }): string {
|
|
23
|
+
return `${model.provider}/${model.id}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Return model references usable by the current main window, with its current
|
|
28
|
+
* model first so it is the deterministic fallback for stale configuration.
|
|
29
|
+
*/
|
|
30
|
+
export function availableModelRefs(ctx: ModelContext): string[] {
|
|
31
|
+
const scoped = ctx.scopedModels.length > 0 ? ctx.scopedModels.map((entry) => entry.model) : undefined;
|
|
32
|
+
const models = scoped ?? ctx.modelRegistry.getAvailable();
|
|
33
|
+
const refs = [...new Set(models.map(modelRef))];
|
|
34
|
+
const currentRef = ctx.model ? modelRef(ctx.model) : undefined;
|
|
35
|
+
if (!currentRef) return refs;
|
|
36
|
+
return [currentRef, ...refs.filter((ref) => ref !== currentRef)];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Replace unavailable persisted overrides with a model usable by the main session. */
|
|
40
|
+
export function repairUnavailableModelOverrides(
|
|
41
|
+
ctx: ModelContext,
|
|
42
|
+
agentModels: Record<string, string>,
|
|
43
|
+
): ModelOverrideRepair {
|
|
44
|
+
const refs = availableModelRefs(ctx);
|
|
45
|
+
const available = new Set(refs);
|
|
46
|
+
const fallbackRef = refs[0];
|
|
47
|
+
const repaired: Record<string, string> = {};
|
|
48
|
+
let changed = false;
|
|
49
|
+
let replaced = 0;
|
|
50
|
+
let removed = 0;
|
|
51
|
+
|
|
52
|
+
for (const [name, configuredRef] of Object.entries(agentModels)) {
|
|
53
|
+
const ref = configuredRef.trim();
|
|
54
|
+
if (available.has(ref)) {
|
|
55
|
+
repaired[name] = ref;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
changed = true;
|
|
60
|
+
if (fallbackRef) {
|
|
61
|
+
repaired[name] = fallbackRef;
|
|
62
|
+
replaced++;
|
|
63
|
+
} else {
|
|
64
|
+
removed++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { agentModels: repaired, changed, replaced, removed, fallbackRef };
|
|
69
|
+
}
|
package/src/monitor.ts
CHANGED
|
@@ -1,238 +1,275 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sub-agent monitor: a module-level singleton store that tracks subagent runs
|
|
3
|
-
* for the current turn.
|
|
4
|
-
*
|
|
5
|
-
* The store notifies subscribers on every mutation so the persistent widget
|
|
6
|
-
* above the editor can re-render. Each run carries timing information
|
|
7
|
-
* (started/ended)
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
/** Epoch ms when the run
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (usage.
|
|
51
|
-
if (usage.
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
if (run.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Sub-agent monitor: a module-level singleton store that tracks subagent runs
|
|
3
|
+
* for the current turn.
|
|
4
|
+
*
|
|
5
|
+
* The store notifies subscribers on every mutation so the persistent widget
|
|
6
|
+
* above the editor can re-render. Each run carries timing information
|
|
7
|
+
* (started/ended) plus a concise activity string describing what the run is
|
|
8
|
+
* doing right now ("thinking", "read src/index.ts", ...). Runs are removed
|
|
9
|
+
* as soon as they finish: the tool result is the durable record in the main
|
|
10
|
+
* conversation, so a stale "done" row must not linger in the widget.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import type { UsageStats } from "./spawn.ts";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export type RunStatus = "queued" | "running" | "done" | "failed";
|
|
21
|
+
|
|
22
|
+
export interface RunView {
|
|
23
|
+
id: number;
|
|
24
|
+
agent: string;
|
|
25
|
+
model?: string;
|
|
26
|
+
status: RunStatus;
|
|
27
|
+
usage: UsageStats;
|
|
28
|
+
/** Concise current activity ("thinking", "read src/index.ts"); last writer wins. */
|
|
29
|
+
activity?: string;
|
|
30
|
+
/** Epoch ms when the run started executing (set on first "running" status). */
|
|
31
|
+
startedAt?: number;
|
|
32
|
+
/** Epoch ms when the run finished (set on "done"/"failed"). */
|
|
33
|
+
endedAt?: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Formatting helpers
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
function formatTokens(count: number): string {
|
|
41
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
42
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
43
|
+
return String(count);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function formatUsageCompact(usage: UsageStats): string {
|
|
47
|
+
const parts: string[] = [];
|
|
48
|
+
if (usage.input) parts.push(`↑${formatTokens(usage.input)}`);
|
|
49
|
+
if (usage.output) parts.push(`↓${formatTokens(usage.output)}`);
|
|
50
|
+
if (usage.cacheRead) parts.push(`R${formatTokens(usage.cacheRead)}`);
|
|
51
|
+
if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
|
|
52
|
+
return parts.join(" ");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatDuration(ms: number): string {
|
|
56
|
+
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
|
57
|
+
if (totalSeconds < 60) return `${totalSeconds}s`;
|
|
58
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
59
|
+
const seconds = totalSeconds % 60;
|
|
60
|
+
if (minutes < 60) return `${minutes}m${String(seconds).padStart(2, "0")}s`;
|
|
61
|
+
const hours = Math.floor(minutes / 60);
|
|
62
|
+
return `${hours}h${String(minutes % 60).padStart(2, "0")}m`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Elapsed wall time of a run: live while running, final once finished. */
|
|
66
|
+
export function formatElapsed(run: RunView, now: number = Date.now()): string {
|
|
67
|
+
if (run.startedAt === undefined) return "";
|
|
68
|
+
const end = run.endedAt ?? now;
|
|
69
|
+
return formatDuration(end - run.startedAt);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Max length of the argument target inside a formatted activity line. */
|
|
73
|
+
export const ACTIVITY_TARGET_MAX = 60;
|
|
74
|
+
|
|
75
|
+
function shortTarget(value: unknown): string {
|
|
76
|
+
if (typeof value !== "string") return "";
|
|
77
|
+
const oneLine = value.replace(/\s+/g, " ").trim();
|
|
78
|
+
// Slice by code point so emoji / CJK-ext never leave a lone surrogate.
|
|
79
|
+
const chars = [...oneLine];
|
|
80
|
+
return chars.length > ACTIVITY_TARGET_MAX ? `${chars.slice(0, ACTIVITY_TARGET_MAX - 1).join("")}…` : oneLine;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Concise "what is it doing" text for a tool call: the tool name plus its single
|
|
84
|
+
* most telling argument (path, command, pattern, ...) — never a raw JSON blob. */
|
|
85
|
+
export function formatToolActivity(toolName: string, args: unknown): string {
|
|
86
|
+
const a = (typeof args === "object" && args !== null ? args : {}) as Record<string, unknown>;
|
|
87
|
+
const pick = (...keys: string[]): string => {
|
|
88
|
+
for (const key of keys) {
|
|
89
|
+
const s = shortTarget(a[key]);
|
|
90
|
+
if (s) return s;
|
|
91
|
+
}
|
|
92
|
+
return "";
|
|
93
|
+
};
|
|
94
|
+
let target: string;
|
|
95
|
+
switch (toolName) {
|
|
96
|
+
case "bash":
|
|
97
|
+
case "shell":
|
|
98
|
+
target = pick("command");
|
|
99
|
+
break;
|
|
100
|
+
case "read":
|
|
101
|
+
case "edit":
|
|
102
|
+
case "write":
|
|
103
|
+
case "ls":
|
|
104
|
+
target = pick("path", "file", "filePath");
|
|
105
|
+
break;
|
|
106
|
+
case "grep":
|
|
107
|
+
case "find":
|
|
108
|
+
case "glob":
|
|
109
|
+
target = pick("pattern", "query", "path");
|
|
110
|
+
break;
|
|
111
|
+
case "web_search":
|
|
112
|
+
case "search":
|
|
113
|
+
target = pick("query");
|
|
114
|
+
break;
|
|
115
|
+
case "fetch":
|
|
116
|
+
case "web_fetch":
|
|
117
|
+
case "fetch_content":
|
|
118
|
+
target = pick("url");
|
|
119
|
+
break;
|
|
120
|
+
case "subagent":
|
|
121
|
+
target = pick("agent", "task");
|
|
122
|
+
break;
|
|
123
|
+
default:
|
|
124
|
+
target = pick("path", "command", "query", "pattern", "url", "file", "task");
|
|
125
|
+
}
|
|
126
|
+
return target ? `${toolName} ${target}` : toolName;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// MonitorStore
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
export class MonitorStore {
|
|
134
|
+
private runs: RunView[] = [];
|
|
135
|
+
private nextId = 1;
|
|
136
|
+
private subscribers = new Set<() => void>();
|
|
137
|
+
|
|
138
|
+
beginTurn(): void {
|
|
139
|
+
// Clear finished runs from a previous turn, but keep any still-active
|
|
140
|
+
// (queued/running) ones so a concurrent sub-agent call is not wiped.
|
|
141
|
+
this.runs = this.runs.filter((r) => r.status === "queued" || r.status === "running");
|
|
142
|
+
this.notify();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
addRun(agent: string, model?: string): number {
|
|
146
|
+
const id = this.nextId++;
|
|
147
|
+
this.runs.push({
|
|
148
|
+
id,
|
|
149
|
+
agent,
|
|
150
|
+
model,
|
|
151
|
+
status: "queued",
|
|
152
|
+
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
153
|
+
});
|
|
154
|
+
this.notify();
|
|
155
|
+
return id;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
setStatus(id: number, status: RunStatus): void {
|
|
159
|
+
const run = this.find(id);
|
|
160
|
+
if (!run) return;
|
|
161
|
+
run.status = status;
|
|
162
|
+
if (status === "running" && run.startedAt === undefined) {
|
|
163
|
+
run.startedAt = Date.now();
|
|
164
|
+
} else if ((status === "done" || status === "failed") && run.endedAt === undefined) {
|
|
165
|
+
run.endedAt = Date.now();
|
|
166
|
+
}
|
|
167
|
+
this.notify();
|
|
168
|
+
}
|
|
169
|
+
setUsage(id: number, usage: UsageStats, model?: string): void {
|
|
170
|
+
const run = this.find(id);
|
|
171
|
+
if (!run) return;
|
|
172
|
+
run.usage = { ...usage };
|
|
173
|
+
if (model) run.model = model;
|
|
174
|
+
this.notify();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Update the run's current one-line activity (what it is doing now). */
|
|
178
|
+
setActivity(id: number, text: string): void {
|
|
179
|
+
const run = this.find(id);
|
|
180
|
+
if (!run) return;
|
|
181
|
+
run.activity = text;
|
|
182
|
+
this.notify();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Remove a run (finished runs leave the widget). Returns the removed run. */
|
|
186
|
+
removeRun(id: number): RunView | undefined {
|
|
187
|
+
const index = this.runs.findIndex((r) => r.id === id);
|
|
188
|
+
if (index === -1) return undefined;
|
|
189
|
+
const [run] = this.runs.splice(index, 1);
|
|
190
|
+
this.notify();
|
|
191
|
+
return run;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
getRuns(): RunView[] {
|
|
195
|
+
return this.runs;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
subscribe(cb: () => void): () => void {
|
|
199
|
+
this.subscribers.add(cb);
|
|
200
|
+
return () => {
|
|
201
|
+
this.subscribers.delete(cb);
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
summarize(run: RunView): string {
|
|
206
|
+
const usage = formatUsageCompact(run.usage);
|
|
207
|
+
const parts = [run.agent];
|
|
208
|
+
if (run.model) parts.push(run.model);
|
|
209
|
+
if (usage) parts.push(usage);
|
|
210
|
+
const elapsed = formatElapsed(run);
|
|
211
|
+
if (elapsed) parts.push(elapsed);
|
|
212
|
+
return parts.join(" · ");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private find(id: number): RunView | undefined {
|
|
216
|
+
return this.runs.find((r) => r.id === id);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private notify(): void {
|
|
220
|
+
for (const cb of this.subscribers) {
|
|
221
|
+
try {
|
|
222
|
+
cb();
|
|
223
|
+
} catch {
|
|
224
|
+
/* subscriber errors must not break the store */
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export const monitor = new MonitorStore();
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Status icons
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
export function statusIcon(status: RunStatus, theme: Theme): string {
|
|
237
|
+
switch (status) {
|
|
238
|
+
case "running":
|
|
239
|
+
return theme.fg("accent", "●");
|
|
240
|
+
case "done":
|
|
241
|
+
return theme.fg("success", "✓");
|
|
242
|
+
case "failed":
|
|
243
|
+
return theme.fg("error", "✗");
|
|
244
|
+
default:
|
|
245
|
+
return theme.fg("dim", "○");
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** User-facing status label shown in the widget. */
|
|
250
|
+
export function statusLabel(status: RunStatus): string {
|
|
251
|
+
switch (status) {
|
|
252
|
+
case "queued":
|
|
253
|
+
return "ready";
|
|
254
|
+
case "running":
|
|
255
|
+
return "running";
|
|
256
|
+
case "done":
|
|
257
|
+
return "done";
|
|
258
|
+
case "failed":
|
|
259
|
+
return "stopped";
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Theme color matching the status label. */
|
|
264
|
+
export function statusColor(status: RunStatus): "accent" | "success" | "error" | "dim" {
|
|
265
|
+
switch (status) {
|
|
266
|
+
case "running":
|
|
267
|
+
return "accent";
|
|
268
|
+
case "done":
|
|
269
|
+
return "success";
|
|
270
|
+
case "failed":
|
|
271
|
+
return "error";
|
|
272
|
+
default:
|
|
273
|
+
return "dim";
|
|
274
|
+
}
|
|
275
|
+
}
|