pi-chalin 0.1.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/README.md +264 -0
- package/agents/conflict-resolver.md +28 -0
- package/agents/context-builder.md +31 -0
- package/agents/delegate.md +28 -0
- package/agents/oracle.md +28 -0
- package/agents/planner.md +28 -0
- package/agents/researcher.md +29 -0
- package/agents/reviewer.md +30 -0
- package/agents/scout.md +32 -0
- package/agents/worker.md +29 -0
- package/package.json +91 -0
- package/src/agent-overrides.ts +12 -0
- package/src/agents.ts +274 -0
- package/src/artifacts.ts +326 -0
- package/src/autoroute.ts +274 -0
- package/src/budget.ts +333 -0
- package/src/child-sessions.ts +108 -0
- package/src/child-tools.ts +796 -0
- package/src/commands.ts +140 -0
- package/src/config.ts +189 -0
- package/src/discovery.ts +190 -0
- package/src/index.ts +40 -0
- package/src/interview.ts +202 -0
- package/src/kernel.ts +254 -0
- package/src/memory.ts +945 -0
- package/src/model-resolution.ts +106 -0
- package/src/orchestration.ts +99 -0
- package/src/paths.ts +50 -0
- package/src/route-format.ts +149 -0
- package/src/route-guards.ts +92 -0
- package/src/route-widget.ts +219 -0
- package/src/runner-prompt.ts +346 -0
- package/src/runner-state.ts +105 -0
- package/src/runner.ts +1185 -0
- package/src/runtime-state.ts +175 -0
- package/src/schemas.ts +316 -0
- package/src/snapshot.ts +282 -0
- package/src/sql-js-fts5.d.ts +4 -0
- package/src/tools.ts +558 -0
- package/src/ui-agents.ts +338 -0
- package/src/ui-status.ts +87 -0
- package/src/ui.ts +875 -0
- package/src/webfetch.ts +294 -0
- package/src/worktrees.ts +113 -0
package/src/ui-agents.ts
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { DynamicBorder, type ExtensionContext, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, fuzzyFilter, getKeybindings, Input, Spacer, Text, type Focusable, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { setAgentModelOverride, setAgentThinkingOverride, type ModelPersistenceTarget } from "./config.ts";
|
|
4
|
+
import type { AgentDefinition, AgentThinkingLevel } from "./schemas.ts";
|
|
5
|
+
|
|
6
|
+
const MODEL_PICKER_VISIBLE_ROWS = 12;
|
|
7
|
+
|
|
8
|
+
type AvailableModel = Awaited<ReturnType<ExtensionContext["modelRegistry"]["getAvailable"]>>[number];
|
|
9
|
+
|
|
10
|
+
interface AgentModelOption {
|
|
11
|
+
value: string;
|
|
12
|
+
provider?: string;
|
|
13
|
+
id?: string;
|
|
14
|
+
name?: string;
|
|
15
|
+
searchText: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function openAgentManager(
|
|
19
|
+
ctx: ExtensionContext,
|
|
20
|
+
agents: AgentDefinition[],
|
|
21
|
+
sessionModelOverrides: Map<string, string>,
|
|
22
|
+
sessionThinkingOverrides: Map<string, AgentThinkingLevel>,
|
|
23
|
+
persistedModelOverrides: Record<string, string> = {},
|
|
24
|
+
persistedThinkingOverrides: Record<string, AgentThinkingLevel> = {},
|
|
25
|
+
): Promise<void> {
|
|
26
|
+
const format = (agent: AgentDefinition) => {
|
|
27
|
+
const key = `${agent.scope}/${agent.name}`;
|
|
28
|
+
const model = effectiveAgentModel(agent, sessionModelOverrides, persistedModelOverrides);
|
|
29
|
+
const thinking = effectiveAgentThinking(agent, sessionThinkingOverrides, persistedThinkingOverrides);
|
|
30
|
+
return `${key} · ${agent.concern} · ${model} · thinking ${thinking}`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (!ctx.hasUI) {
|
|
34
|
+
ctx.ui.notify(agents.map(format).join("\n") || "No agents found", "info");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const selected = await ctx.ui.select("pi-chalin Agents", agents.map(format));
|
|
39
|
+
if (!selected) return;
|
|
40
|
+
const agent = agents.find((candidate) => selected.startsWith(`${candidate.scope}/${candidate.name} ·`));
|
|
41
|
+
if (!agent) return;
|
|
42
|
+
|
|
43
|
+
const action = await ctx.ui.select(`${agent.scope}/${agent.name}`, ["Inspect", "Change model", "Change thinking", "Reset model", "Reset thinking", "Close"]);
|
|
44
|
+
if (action === "Change model") return openAgentModelPicker(ctx, agent, sessionModelOverrides, persistedModelOverrides);
|
|
45
|
+
if (action === "Change thinking") return openAgentThinkingPicker(ctx, agent, sessionThinkingOverrides);
|
|
46
|
+
if (action === "Reset model") {
|
|
47
|
+
const key = `${agent.scope}/${agent.name}`;
|
|
48
|
+
sessionModelOverrides.delete(key);
|
|
49
|
+
setAgentModelOverride({ cwd: ctx.cwd }, key, undefined, defaultPersistenceTarget(agent.scope) === "user" ? "user" : "project");
|
|
50
|
+
ctx.ui.notify(`${key} reset to inherit.`, "info");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (action === "Reset thinking") {
|
|
54
|
+
const key = `${agent.scope}/${agent.name}`;
|
|
55
|
+
sessionThinkingOverrides.delete(key);
|
|
56
|
+
setAgentThinkingOverride({ cwd: ctx.cwd }, key, undefined, defaultPersistenceTarget(agent.scope) === "user" ? "user" : "project");
|
|
57
|
+
ctx.ui.notify(`${key} thinking reset to inherit.`, "info");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (action === "Inspect") {
|
|
61
|
+
ctx.ui.notify(
|
|
62
|
+
[
|
|
63
|
+
`${agent.scope}/${agent.name}`,
|
|
64
|
+
agent.description,
|
|
65
|
+
`concern: ${agent.concern}`,
|
|
66
|
+
`model: ${effectiveAgentModel(agent, sessionModelOverrides, persistedModelOverrides)}`,
|
|
67
|
+
`thinking: ${effectiveAgentThinking(agent, sessionThinkingOverrides, persistedThinkingOverrides)}`,
|
|
68
|
+
`tools: ${agent.tools.join(", ") || "none"}`,
|
|
69
|
+
`memory: read=${agent.memory.read}, write=${agent.memory.write}`,
|
|
70
|
+
agent.sourcePath ? `source: ${agent.sourcePath}` : undefined,
|
|
71
|
+
"keys: ↑/↓ select · enter open · esc close",
|
|
72
|
+
].filter((line): line is string => Boolean(line)).join("\n"),
|
|
73
|
+
agent.diagnostics.length > 0 ? "warning" : "info",
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function openAgentThinkingPicker(
|
|
79
|
+
ctx: ExtensionContext,
|
|
80
|
+
agent: AgentDefinition,
|
|
81
|
+
sessionThinkingOverrides: Map<string, AgentThinkingLevel>,
|
|
82
|
+
): Promise<void> {
|
|
83
|
+
const key = `${agent.scope}/${agent.name}`;
|
|
84
|
+
const options: AgentThinkingLevel[] = ["inherit", "off", "minimal", "low", "medium", "high", "xhigh"];
|
|
85
|
+
const selected = ctx.hasUI ? await ctx.ui.select(`Thinking for ${key}`, options) as AgentThinkingLevel | undefined : undefined;
|
|
86
|
+
if (!selected) return;
|
|
87
|
+
|
|
88
|
+
const target = await choosePersistenceTarget(ctx, agent.scope);
|
|
89
|
+
if (!target) return;
|
|
90
|
+
if (target === "session") {
|
|
91
|
+
if (selected === "inherit") sessionThinkingOverrides.delete(key);
|
|
92
|
+
else sessionThinkingOverrides.set(key, selected);
|
|
93
|
+
} else {
|
|
94
|
+
setAgentThinkingOverride({ cwd: ctx.cwd }, key, selected, target);
|
|
95
|
+
}
|
|
96
|
+
ctx.ui.notify(`${key} thinking set to ${selected} (${target}).`, "info");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function openAgentModelPicker(
|
|
100
|
+
ctx: ExtensionContext,
|
|
101
|
+
agent: AgentDefinition,
|
|
102
|
+
sessionModelOverrides: Map<string, string>,
|
|
103
|
+
persistedModelOverrides: Record<string, string> = {},
|
|
104
|
+
): Promise<void> {
|
|
105
|
+
const key = `${agent.scope}/${agent.name}`;
|
|
106
|
+
const available = loadAvailableAgentModels(ctx);
|
|
107
|
+
const current = effectiveAgentModel(agent, sessionModelOverrides, persistedModelOverrides);
|
|
108
|
+
const options = buildAgentModelOptions(available, current);
|
|
109
|
+
const selected = ctx.hasUI ? await openAgentModelSearchPicker(ctx, `Model for ${key}`, options, current) : undefined;
|
|
110
|
+
if (!selected) return;
|
|
111
|
+
if (!options.some((option) => option.value === selected)) {
|
|
112
|
+
ctx.ui.notify(`Model '${selected}' is not available.`, "error");
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const target = await choosePersistenceTarget(ctx, agent.scope);
|
|
117
|
+
if (!target) return;
|
|
118
|
+
if (target === "session") {
|
|
119
|
+
if (selected === "inherit") sessionModelOverrides.delete(key);
|
|
120
|
+
else sessionModelOverrides.set(key, selected);
|
|
121
|
+
} else {
|
|
122
|
+
setAgentModelOverride({ cwd: ctx.cwd }, key, selected === "inherit" ? undefined : selected, target);
|
|
123
|
+
}
|
|
124
|
+
ctx.ui.notify(`${key} model set to ${selected} (${target}).`, "info");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function effectiveAgentModel(
|
|
128
|
+
agent: AgentDefinition,
|
|
129
|
+
sessionModelOverrides: Map<string, string>,
|
|
130
|
+
persistedModelOverrides: Record<string, string>,
|
|
131
|
+
): string {
|
|
132
|
+
const key = `${agent.scope}/${agent.name}`;
|
|
133
|
+
return sessionModelOverrides.get(key)
|
|
134
|
+
?? persistedModelOverrides[key]
|
|
135
|
+
?? persistedModelOverrides[agent.name]
|
|
136
|
+
?? agent.model;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function effectiveAgentThinking(
|
|
140
|
+
agent: AgentDefinition,
|
|
141
|
+
sessionThinkingOverrides: Map<string, AgentThinkingLevel>,
|
|
142
|
+
persistedThinkingOverrides: Record<string, AgentThinkingLevel>,
|
|
143
|
+
): AgentThinkingLevel {
|
|
144
|
+
const key = `${agent.scope}/${agent.name}`;
|
|
145
|
+
return sessionThinkingOverrides.get(key)
|
|
146
|
+
?? persistedThinkingOverrides[key]
|
|
147
|
+
?? persistedThinkingOverrides[agent.name]
|
|
148
|
+
?? agent.thinking
|
|
149
|
+
?? "inherit";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function loadAvailableAgentModels(ctx: ExtensionContext): AvailableModel[] {
|
|
153
|
+
ctx.modelRegistry.refresh();
|
|
154
|
+
const loadError = ctx.modelRegistry.getError?.();
|
|
155
|
+
if (ctx.hasUI && loadError) ctx.ui.notify(`Warning: errors loading models.json:\n${loadError}`, "warning");
|
|
156
|
+
return ctx.modelRegistry.getAvailable();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildAgentModelOptions(models: AvailableModel[], current: string | undefined): AgentModelOption[] {
|
|
160
|
+
const normalizedCurrent = current && current !== "inherit" ? current : undefined;
|
|
161
|
+
const sortedModels = [...models].sort((a, b) => {
|
|
162
|
+
const aValue = `${a.provider}/${a.id}`;
|
|
163
|
+
const bValue = `${b.provider}/${b.id}`;
|
|
164
|
+
if (normalizedCurrent) {
|
|
165
|
+
if (aValue === normalizedCurrent && bValue !== normalizedCurrent) return -1;
|
|
166
|
+
if (bValue === normalizedCurrent && aValue !== normalizedCurrent) return 1;
|
|
167
|
+
}
|
|
168
|
+
const provider = a.provider.localeCompare(b.provider);
|
|
169
|
+
return provider || a.id.localeCompare(b.id);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
return [
|
|
173
|
+
{ value: "inherit", searchText: "inherit default parent agent model" },
|
|
174
|
+
...sortedModels.map((model) => ({
|
|
175
|
+
value: `${model.provider}/${model.id}`,
|
|
176
|
+
provider: model.provider,
|
|
177
|
+
id: model.id,
|
|
178
|
+
name: model.name,
|
|
179
|
+
searchText: `${model.provider} ${model.id} ${model.provider}/${model.id} ${model.name ?? ""}`,
|
|
180
|
+
})),
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function openAgentModelSearchPicker(
|
|
185
|
+
ctx: ExtensionContext,
|
|
186
|
+
title: string,
|
|
187
|
+
options: AgentModelOption[],
|
|
188
|
+
current: string | undefined,
|
|
189
|
+
): Promise<string | undefined> {
|
|
190
|
+
if (options.length <= 1) {
|
|
191
|
+
ctx.ui.notify("No authenticated models are available. Use /login or configure ~/.pi/agent/models.json.", "warning");
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => new AgentModelPickerComponent(tui, theme, title, options, current, done));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
class AgentModelPickerComponent extends Container implements Focusable {
|
|
199
|
+
private searchInput = new Input();
|
|
200
|
+
private listContainer = new Container();
|
|
201
|
+
private filteredOptions: AgentModelOption[];
|
|
202
|
+
private selectedIndex = 0;
|
|
203
|
+
private _focused = false;
|
|
204
|
+
|
|
205
|
+
get focused(): boolean {
|
|
206
|
+
return this._focused;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
set focused(value: boolean) {
|
|
210
|
+
this._focused = value;
|
|
211
|
+
this.searchInput.focused = value;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
constructor(
|
|
215
|
+
private tui: TUI,
|
|
216
|
+
private theme: Theme,
|
|
217
|
+
title: string,
|
|
218
|
+
private options: AgentModelOption[],
|
|
219
|
+
private current: string | undefined,
|
|
220
|
+
private done: (selected: string | undefined) => void,
|
|
221
|
+
) {
|
|
222
|
+
super();
|
|
223
|
+
this.filteredOptions = options;
|
|
224
|
+
this.selectedIndex = Math.max(0, options.findIndex((option) => option.value === (current ?? "inherit")));
|
|
225
|
+
|
|
226
|
+
this.addChild(new DynamicBorder((text) => this.theme.fg("borderAccent", text)));
|
|
227
|
+
this.addChild(new Spacer(1));
|
|
228
|
+
this.addChild(new Text(this.theme.fg("accent", this.theme.bold(title)), 1, 0));
|
|
229
|
+
this.addChild(new Spacer(1));
|
|
230
|
+
this.addChild(this.searchInput);
|
|
231
|
+
this.addChild(new Spacer(1));
|
|
232
|
+
this.addChild(this.listContainer);
|
|
233
|
+
this.addChild(new Spacer(1));
|
|
234
|
+
this.addChild(
|
|
235
|
+
new Text(
|
|
236
|
+
this.keyHint("↑/↓", "navigate")
|
|
237
|
+
+ " "
|
|
238
|
+
+ this.keyHint("enter", "select")
|
|
239
|
+
+ " "
|
|
240
|
+
+ this.keyHint("escape/ctrl+c", "cancel"),
|
|
241
|
+
1,
|
|
242
|
+
0,
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
this.addChild(new Spacer(1));
|
|
246
|
+
this.addChild(new DynamicBorder((text) => this.theme.fg("borderAccent", text)));
|
|
247
|
+
|
|
248
|
+
this.updateList();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
handleInput(keyData: string): void {
|
|
252
|
+
const kb = getKeybindings();
|
|
253
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
254
|
+
if (this.filteredOptions.length > 0) {
|
|
255
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.filteredOptions.length - 1 : this.selectedIndex - 1;
|
|
256
|
+
this.updateList();
|
|
257
|
+
}
|
|
258
|
+
} else if (kb.matches(keyData, "tui.select.down")) {
|
|
259
|
+
if (this.filteredOptions.length > 0) {
|
|
260
|
+
this.selectedIndex = this.selectedIndex === this.filteredOptions.length - 1 ? 0 : this.selectedIndex + 1;
|
|
261
|
+
this.updateList();
|
|
262
|
+
}
|
|
263
|
+
} else if (kb.matches(keyData, "tui.select.pageUp")) {
|
|
264
|
+
this.selectedIndex = Math.max(0, this.selectedIndex - MODEL_PICKER_VISIBLE_ROWS);
|
|
265
|
+
this.updateList();
|
|
266
|
+
} else if (kb.matches(keyData, "tui.select.pageDown")) {
|
|
267
|
+
this.selectedIndex = Math.min(Math.max(0, this.filteredOptions.length - 1), this.selectedIndex + MODEL_PICKER_VISIBLE_ROWS);
|
|
268
|
+
this.updateList();
|
|
269
|
+
} else if (kb.matches(keyData, "tui.select.confirm")) {
|
|
270
|
+
this.done(this.filteredOptions[this.selectedIndex]?.value);
|
|
271
|
+
return;
|
|
272
|
+
} else if (kb.matches(keyData, "tui.select.cancel")) {
|
|
273
|
+
this.done(undefined);
|
|
274
|
+
return;
|
|
275
|
+
} else {
|
|
276
|
+
this.searchInput.handleInput(keyData);
|
|
277
|
+
this.filterOptions(this.searchInput.getValue());
|
|
278
|
+
this.updateList();
|
|
279
|
+
}
|
|
280
|
+
this.tui.requestRender();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private filterOptions(query: string): void {
|
|
284
|
+
this.filteredOptions = query
|
|
285
|
+
? fuzzyFilter(this.options, query, (option) => option.searchText)
|
|
286
|
+
: this.options;
|
|
287
|
+
this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredOptions.length - 1));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private updateList(): void {
|
|
291
|
+
this.listContainer.clear();
|
|
292
|
+
if (this.filteredOptions.length === 0) {
|
|
293
|
+
this.listContainer.addChild(new Text(this.theme.fg("muted", " No matching models"), 1, 0));
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const startIndex = Math.max(
|
|
298
|
+
0,
|
|
299
|
+
Math.min(this.selectedIndex - Math.floor(MODEL_PICKER_VISIBLE_ROWS / 2), this.filteredOptions.length - MODEL_PICKER_VISIBLE_ROWS),
|
|
300
|
+
);
|
|
301
|
+
const endIndex = Math.min(startIndex + MODEL_PICKER_VISIBLE_ROWS, this.filteredOptions.length);
|
|
302
|
+
|
|
303
|
+
for (let index = startIndex; index < endIndex; index += 1) {
|
|
304
|
+
const option = this.filteredOptions[index];
|
|
305
|
+
if (!option) continue;
|
|
306
|
+
const isSelected = index === this.selectedIndex;
|
|
307
|
+
const prefix = isSelected ? this.theme.fg("accent", "→ ") : " ";
|
|
308
|
+
const text = formatAgentModelOption(option);
|
|
309
|
+
const currentMarker = option.value === (this.current ?? "inherit") ? this.theme.fg("success", " ✓") : "";
|
|
310
|
+
this.listContainer.addChild(new Text(prefix + (isSelected ? this.theme.fg("accent", text) : this.theme.fg("text", text)) + currentMarker, 1, 0));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (startIndex > 0 || endIndex < this.filteredOptions.length) {
|
|
314
|
+
this.listContainer.addChild(new Text(this.theme.fg("muted", ` (${this.selectedIndex + 1}/${this.filteredOptions.length})`), 1, 0));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private keyHint(key: string, description: string): string {
|
|
319
|
+
return this.theme.fg("dim", key) + this.theme.fg("muted", ` ${description}`);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function formatAgentModelOption(option: AgentModelOption): string {
|
|
324
|
+
if (option.value === "inherit") return "inherit";
|
|
325
|
+
const name = option.name && option.name !== option.id ? ` · ${option.name}` : "";
|
|
326
|
+
return `${option.provider}/${option.id}${name}`;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function choosePersistenceTarget(ctx: ExtensionContext, scope: AgentDefinition["scope"]): Promise<ModelPersistenceTarget | undefined> {
|
|
330
|
+
const preferred = defaultPersistenceTarget(scope);
|
|
331
|
+
const options: ModelPersistenceTarget[] = [preferred, ...(["session", "project", "user"] as const).filter((item) => item !== preferred)];
|
|
332
|
+
const selected = ctx.hasUI ? await ctx.ui.select("Persist model selection", options) : preferred;
|
|
333
|
+
return selected as ModelPersistenceTarget | undefined;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function defaultPersistenceTarget(scope: AgentDefinition["scope"]): Exclude<ModelPersistenceTarget, "session"> {
|
|
337
|
+
return scope === "project" ? "project" : "user";
|
|
338
|
+
}
|
package/src/ui-status.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
const FOOTER_FRAMES = ["◆", "◇"];
|
|
4
|
+
const FOOTER_ANIMATION_MS = 650;
|
|
5
|
+
const LEGACY_CONTROL_WIDGET_KEY = "pi-chalin-control";
|
|
6
|
+
|
|
7
|
+
let footerTimer: ReturnType<typeof setInterval> | undefined;
|
|
8
|
+
let footerFrame = 0;
|
|
9
|
+
let footerTarget: Pick<ExtensionContext, "hasUI" | "ui"> | undefined;
|
|
10
|
+
let footerState: ChalinFooterState = { kind: "idle" };
|
|
11
|
+
|
|
12
|
+
export type ChalinFooterState =
|
|
13
|
+
| { kind: "idle" }
|
|
14
|
+
| { kind: "off" }
|
|
15
|
+
| { kind: "on" }
|
|
16
|
+
| { kind: "running"; intent: string; agent: string; completed: number; total: number }
|
|
17
|
+
| { kind: "synthesizing" }
|
|
18
|
+
| { kind: "complete"; intent?: string }
|
|
19
|
+
| { kind: "stopped" }
|
|
20
|
+
| { kind: "failed" };
|
|
21
|
+
|
|
22
|
+
export function clearLegacyChalinControlWidget(ctx: Pick<ExtensionContext, "hasUI" | "ui">): void {
|
|
23
|
+
if (!ctx.hasUI) return;
|
|
24
|
+
ctx.ui.setWidget(LEGACY_CONTROL_WIDGET_KEY, undefined);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function setChalinStatus(ctx: Pick<ExtensionContext, "hasUI" | "ui">, state: ChalinFooterState | string | undefined): void {
|
|
28
|
+
if (!ctx.hasUI) return;
|
|
29
|
+
if (state === undefined) {
|
|
30
|
+
stopFooterAnimation();
|
|
31
|
+
ctx.ui.setStatus("pi-chalin", undefined);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
footerTarget = ctx;
|
|
35
|
+
footerState = typeof state === "string" ? parseLegacyChalinStatus(state) : state;
|
|
36
|
+
renderChalinFooterStatus();
|
|
37
|
+
if (footerState.kind === "running" || footerState.kind === "synthesizing") startFooterAnimation();
|
|
38
|
+
else stopFooterAnimation(false);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function chalinFooterText(state: ChalinFooterState, frame = 0): string {
|
|
42
|
+
if (state.kind === "idle") return "chalin ◦ idle";
|
|
43
|
+
if (state.kind === "off") return "chalin × off";
|
|
44
|
+
if (state.kind === "on") return "chalin ◦ ready";
|
|
45
|
+
if (state.kind === "stopped") return "chalin ■ stopped";
|
|
46
|
+
if (state.kind === "failed") return "chalin × failed";
|
|
47
|
+
if (state.kind === "complete") return state.intent ? `chalin ✓ ${state.intent}` : "chalin ✓ complete";
|
|
48
|
+
if (state.kind === "synthesizing") return `chalin ${FOOTER_FRAMES[frame % FOOTER_FRAMES.length]} synthesizing`;
|
|
49
|
+
return `chalin ${FOOTER_FRAMES[frame % FOOTER_FRAMES.length]} ${state.intent} · ${state.agent} ${state.completed}/${state.total}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseLegacyChalinStatus(text: string): ChalinFooterState {
|
|
53
|
+
if (/off$/i.test(text)) return { kind: "off" };
|
|
54
|
+
if (/on$/i.test(text)) return { kind: "on" };
|
|
55
|
+
if (/idle$/i.test(text)) return { kind: "idle" };
|
|
56
|
+
if (/stopped$/i.test(text)) return { kind: "stopped" };
|
|
57
|
+
if (/failed$/i.test(text)) return { kind: "failed" };
|
|
58
|
+
if (/consolidating|synth/i.test(text)) return { kind: "synthesizing" };
|
|
59
|
+
if (/running$/i.test(text)) return { kind: "running", intent: "working", agent: text.replace(/^chalin:\s*/i, "").replace(/\s*running$/i, ""), completed: 0, total: 1 };
|
|
60
|
+
return { kind: "idle" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function startFooterAnimation(): void {
|
|
64
|
+
if (footerTimer) return;
|
|
65
|
+
footerTimer = setInterval(() => {
|
|
66
|
+
footerFrame += 1;
|
|
67
|
+
renderChalinFooterStatus();
|
|
68
|
+
}, FOOTER_ANIMATION_MS);
|
|
69
|
+
timerUnref(footerTimer);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stopFooterAnimation(render = true): void {
|
|
73
|
+
if (footerTimer) {
|
|
74
|
+
clearInterval(footerTimer);
|
|
75
|
+
footerTimer = undefined;
|
|
76
|
+
}
|
|
77
|
+
footerFrame = 0;
|
|
78
|
+
if (render) renderChalinFooterStatus();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function renderChalinFooterStatus(): void {
|
|
82
|
+
footerTarget?.ui.setStatus("pi-chalin", chalinFooterText(footerState, footerFrame));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function timerUnref(timer: ReturnType<typeof setInterval>): void {
|
|
86
|
+
timer.unref?.();
|
|
87
|
+
}
|