shariq-pi-extensions 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/LICENSE +21 -0
- package/README.md +94 -0
- package/THIRD_PARTY_NOTICES.md +11 -0
- package/docs/ARCHITECTURE.md +54 -0
- package/docs/DEVELOPMENT.md +88 -0
- package/docs/EXTENSIONS.md +89 -0
- package/extensions/antigravity-provider/LICENSE +21 -0
- package/extensions/antigravity-provider/README.md +23 -0
- package/extensions/antigravity-provider/antigravity/cloud-code-assist.ts +519 -0
- package/extensions/antigravity-provider/antigravity/index.ts +67 -0
- package/extensions/antigravity-provider/antigravity/models.ts +184 -0
- package/extensions/antigravity-provider/antigravity/oauth.ts +499 -0
- package/extensions/antigravity-provider/index.ts +3 -0
- package/extensions/ask-user/README.md +21 -0
- package/extensions/ask-user/index.ts +417 -0
- package/extensions/background-terminals/README.md +43 -0
- package/extensions/background-terminals/index.ts +400 -0
- package/extensions/background-terminals/src/manager.ts +480 -0
- package/extensions/background-terminals/src/output-buffer.ts +110 -0
- package/extensions/background-terminals/src/presentation.ts +74 -0
- package/extensions/background-terminals/src/types.ts +47 -0
- package/extensions/background-terminals/src/ui.ts +478 -0
- package/extensions/context-usage/LICENSE +21 -0
- package/extensions/context-usage/README.md +20 -0
- package/extensions/context-usage/index.ts +2006 -0
- package/extensions/context-usage/lib/ansi.ts +60 -0
- package/extensions/context-usage/lib/boundary.ts +27 -0
- package/extensions/context-usage/lib/chat.ts +190 -0
- package/extensions/context-usage/lib/config.ts +30 -0
- package/extensions/context-usage/lib/fmt.ts +59 -0
- package/extensions/context-usage/lib/style.ts +260 -0
- package/extensions/copy-all/README.md +15 -0
- package/extensions/copy-all/index.ts +72 -0
- package/extensions/factory-provider/API_KEYS.md +17 -0
- package/extensions/factory-provider/AUTH.md +15 -0
- package/extensions/factory-provider/README.md +90 -0
- package/extensions/factory-provider/REFRESH_MODELS.md +185 -0
- package/extensions/factory-provider/UPDATING.md +97 -0
- package/extensions/factory-provider/factory/api-keys.ts +407 -0
- package/extensions/factory-provider/factory/auth.ts +132 -0
- package/extensions/factory-provider/factory/constants.ts +15 -0
- package/extensions/factory-provider/factory/dashboard.ts +363 -0
- package/extensions/factory-provider/factory/droid.ts +84 -0
- package/extensions/factory-provider/factory/limits.ts +342 -0
- package/extensions/factory-provider/factory/models.ts +283 -0
- package/extensions/factory-provider/factory/responses.ts +331 -0
- package/extensions/factory-provider/factory/warm-theme.ts +53 -0
- package/extensions/factory-provider/factory/websocket.ts +130 -0
- package/extensions/factory-provider/index.ts +204 -0
- package/extensions/factory-provider/scripts/audit-reasoning-efforts.py +84 -0
- package/extensions/firecrawl-web/README.md +31 -0
- package/extensions/firecrawl-web/auth.ts +92 -0
- package/extensions/firecrawl-web/client.ts +238 -0
- package/extensions/firecrawl-web/index.ts +96 -0
- package/extensions/firecrawl-web/output.ts +125 -0
- package/extensions/git-info/README.md +25 -0
- package/extensions/git-info/index.ts +162 -0
- package/extensions/git-info/src/git.ts +206 -0
- package/extensions/git-info/src/ui.ts +291 -0
- package/extensions/goal/README.md +44 -0
- package/extensions/goal/constants.ts +4 -0
- package/extensions/goal/extension.ts +1064 -0
- package/extensions/goal/index.ts +2 -0
- package/extensions/goal/types.ts +56 -0
- package/extensions/goal/ui.ts +334 -0
- package/extensions/orchestration/README.md +58 -0
- package/extensions/orchestration/dashboard.ts +192 -0
- package/extensions/orchestration/engine.ts +706 -0
- package/extensions/orchestration/index.ts +243 -0
- package/extensions/orchestration/prompts.ts +124 -0
- package/extensions/orchestration/protocol.ts +116 -0
- package/extensions/orchestration/settings-ui.ts +41 -0
- package/extensions/orchestration/settings.ts +59 -0
- package/extensions/orchestration/storage.ts +49 -0
- package/extensions/orchestration/types.ts +105 -0
- package/extensions/performance-status/README.md +5 -0
- package/extensions/performance-status/index.ts +171 -0
- package/extensions/pi-memory/README.md +53 -0
- package/extensions/pi-memory/index.ts +326 -0
- package/extensions/pi-memory/src/config.ts +75 -0
- package/extensions/pi-memory/src/database.ts +617 -0
- package/extensions/pi-memory/src/extraction.ts +178 -0
- package/extensions/pi-memory/src/project.ts +80 -0
- package/extensions/pi-memory/src/projection.ts +59 -0
- package/extensions/pi-memory/src/redaction.ts +60 -0
- package/extensions/pi-memory/src/retrieval.ts +30 -0
- package/extensions/pi-memory/src/service.ts +194 -0
- package/extensions/pi-memory/src/session.ts +146 -0
- package/extensions/pi-memory/src/types.ts +82 -0
- package/extensions/shared/README.md +18 -0
- package/extensions/shared/activity-status.ts +31 -0
- package/extensions/shared/child-session.ts +150 -0
- package/extensions/shared/context-utilization.ts +47 -0
- package/extensions/shared/dashboard-state.ts +99 -0
- package/extensions/shared/tool-call-timeout.ts +104 -0
- package/extensions/shared/tui-dashboard.ts +139 -0
- package/extensions/shell-shortcuts/README.md +17 -0
- package/extensions/shell-shortcuts/features/exit-alias.ts +10 -0
- package/extensions/shell-shortcuts/index.ts +6 -0
- package/extensions/subagents/README.md +89 -0
- package/extensions/subagents/index.ts +1386 -0
- package/extensions/subagents/src/backend.ts +69 -0
- package/extensions/subagents/src/backends/pi.ts +781 -0
- package/extensions/subagents/src/catalog.ts +85 -0
- package/extensions/subagents/src/config.ts +241 -0
- package/extensions/subagents/src/context.ts +57 -0
- package/extensions/subagents/src/coordinator.ts +64 -0
- package/extensions/subagents/src/domain.ts +294 -0
- package/extensions/subagents/src/format.ts +74 -0
- package/extensions/subagents/src/manager.ts +813 -0
- package/extensions/subagents/src/prompt.ts +94 -0
- package/extensions/subagents/src/result-delivery.ts +20 -0
- package/extensions/subagents/src/runtime.ts +48 -0
- package/extensions/subagents/src/ui/takeover.ts +695 -0
- package/extensions/subagents/src/ui/transcript.ts +182 -0
- package/extensions/subagents/src/worktree.ts +175 -0
- package/extensions/web-fetch/README.md +13 -0
- package/extensions/web-fetch/index.ts +441 -0
- package/package.json +117 -0
- package/skills/background-terminals/SKILL.md +42 -0
- package/skills/orchestration/SKILL.md +33 -0
- package/skills/subagents/SKILL.md +89 -0
- package/themes/ember-warm-dark.json +85 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionContext,
|
|
3
|
+
KeybindingsManager,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import {
|
|
6
|
+
truncateToWidth,
|
|
7
|
+
type Component,
|
|
8
|
+
type TUI,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
import {
|
|
11
|
+
frameBottom,
|
|
12
|
+
frameTop,
|
|
13
|
+
joinSides,
|
|
14
|
+
meter,
|
|
15
|
+
oneLine,
|
|
16
|
+
padLine,
|
|
17
|
+
stateLabel,
|
|
18
|
+
} from "../../shared/tui-dashboard.ts";
|
|
19
|
+
import {
|
|
20
|
+
exhaustedBucket,
|
|
21
|
+
type FactoryLimitRecord,
|
|
22
|
+
type FactoryPoolLimits,
|
|
23
|
+
} from "./limits.ts";
|
|
24
|
+
import { factoryWarmTheme } from "./warm-theme.ts";
|
|
25
|
+
|
|
26
|
+
export interface FactoryDashboardAccount {
|
|
27
|
+
id: string;
|
|
28
|
+
label: string;
|
|
29
|
+
record?: FactoryLimitRecord;
|
|
30
|
+
cooldownSeconds?: number;
|
|
31
|
+
lastUsedAt?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface FactoryDashboardSnapshot {
|
|
35
|
+
version: string;
|
|
36
|
+
modelCount: number;
|
|
37
|
+
authentication: string;
|
|
38
|
+
configured: number;
|
|
39
|
+
active: number;
|
|
40
|
+
warning?: string;
|
|
41
|
+
accounts: FactoryDashboardAccount[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
type Theme = ExtensionContext["ui"]["theme"];
|
|
45
|
+
|
|
46
|
+
function bucketActive(
|
|
47
|
+
bucket: { secondsRemaining?: number | null; windowEnd?: string | null },
|
|
48
|
+
now = Date.now(),
|
|
49
|
+
observedAt = now,
|
|
50
|
+
) {
|
|
51
|
+
if (bucket.windowEnd) {
|
|
52
|
+
const end = Date.parse(bucket.windowEnd);
|
|
53
|
+
if (Number.isFinite(end)) return end > now;
|
|
54
|
+
}
|
|
55
|
+
return typeof bucket.secondsRemaining === "number" &&
|
|
56
|
+
observedAt + bucket.secondsRemaining * 1000 > now;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function remainingText(
|
|
60
|
+
bucket: { secondsRemaining?: number | null; windowEnd?: string | null },
|
|
61
|
+
now = Date.now(),
|
|
62
|
+
observedAt = now,
|
|
63
|
+
) {
|
|
64
|
+
const resetAt = bucket.windowEnd
|
|
65
|
+
? Date.parse(bucket.windowEnd)
|
|
66
|
+
: observedAt + (bucket.secondsRemaining ?? 0) * 1000;
|
|
67
|
+
const seconds = Number.isFinite(resetAt)
|
|
68
|
+
? Math.max(0, Math.round((resetAt - now) / 1000))
|
|
69
|
+
: 0;
|
|
70
|
+
if (!seconds || !bucketActive(bucket, now, observedAt)) return "inactive";
|
|
71
|
+
const days = Math.floor(seconds / 86_400);
|
|
72
|
+
const hours = Math.floor((seconds % 86_400) / 3_600);
|
|
73
|
+
const minutes = Math.floor((seconds % 3_600) / 60);
|
|
74
|
+
if (days) return `${days}d ${hours}h`;
|
|
75
|
+
if (hours) return `${hours}h ${minutes}m`;
|
|
76
|
+
return `${minutes}m`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function compactPool(
|
|
80
|
+
prefix: string,
|
|
81
|
+
limits: FactoryPoolLimits | undefined,
|
|
82
|
+
observedAt = Date.now(),
|
|
83
|
+
) {
|
|
84
|
+
if (!limits) return `${prefix} usage unavailable`;
|
|
85
|
+
const exhausted = exhaustedBucket(limits, Date.now(), observedAt);
|
|
86
|
+
if (exhausted === "monthly") return `${prefix} used · M 100% · children inactive`;
|
|
87
|
+
if (exhausted === "weekly") {
|
|
88
|
+
return `${prefix} used · M ${limits.monthly.usedPercent}% · W 100% · 5h inactive`;
|
|
89
|
+
}
|
|
90
|
+
const parts = [
|
|
91
|
+
bucketActive(limits.monthly, Date.now(), observedAt) ? `M ${limits.monthly.usedPercent}%` : undefined,
|
|
92
|
+
bucketActive(limits.weekly, Date.now(), observedAt) ? `W ${limits.weekly.usedPercent}%` : undefined,
|
|
93
|
+
bucketActive(limits.fiveHour, Date.now(), observedAt) ? `5h ${limits.fiveHour.usedPercent}%` : undefined,
|
|
94
|
+
].filter(Boolean);
|
|
95
|
+
return `${prefix} used · ${parts.join(" · ") || "fresh window"}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function accountState(account: FactoryDashboardAccount) {
|
|
99
|
+
if (account.cooldownSeconds && account.cooldownSeconds > 0) return "cooldown";
|
|
100
|
+
if (account.record?.error) return "refresh error";
|
|
101
|
+
const observedAt = account.record?.fetchedAt ?? Date.now();
|
|
102
|
+
const standard = exhaustedBucket(account.record?.limits?.standard, Date.now(), observedAt);
|
|
103
|
+
const core = exhaustedBucket(account.record?.limits?.core, Date.now(), observedAt);
|
|
104
|
+
if (standard && core) return "exhausted";
|
|
105
|
+
if (standard || core) return `${standard ? "standard" : "core"} exhausted`;
|
|
106
|
+
return account.record?.limits ? "ready" : "not refreshed";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function stateColor(state: string) {
|
|
110
|
+
if (state === "ready") return "success" as const;
|
|
111
|
+
if (state === "not refreshed") return "muted" as const;
|
|
112
|
+
if (state === "refresh error") return "error" as const;
|
|
113
|
+
return "warning" as const;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class FactoryDashboard implements Component {
|
|
117
|
+
private selected = 0;
|
|
118
|
+
private refreshing = false;
|
|
119
|
+
private closed = false;
|
|
120
|
+
private readonly tui: TUI;
|
|
121
|
+
private readonly theme: Theme;
|
|
122
|
+
private readonly keys: KeybindingsManager;
|
|
123
|
+
private snapshot: FactoryDashboardSnapshot;
|
|
124
|
+
private readonly refreshData: (force: boolean) => Promise<FactoryDashboardSnapshot>;
|
|
125
|
+
private readonly done: () => void;
|
|
126
|
+
|
|
127
|
+
constructor(
|
|
128
|
+
tui: TUI,
|
|
129
|
+
theme: Theme,
|
|
130
|
+
keys: KeybindingsManager,
|
|
131
|
+
snapshot: FactoryDashboardSnapshot,
|
|
132
|
+
refreshData: (force: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
133
|
+
done: () => void,
|
|
134
|
+
) {
|
|
135
|
+
this.tui = tui;
|
|
136
|
+
this.theme = factoryWarmTheme(theme);
|
|
137
|
+
this.keys = keys;
|
|
138
|
+
this.snapshot = snapshot;
|
|
139
|
+
this.refreshData = refreshData;
|
|
140
|
+
this.done = done;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
startRefresh(force: boolean) {
|
|
144
|
+
if (this.refreshing || this.closed) return;
|
|
145
|
+
this.refreshing = true;
|
|
146
|
+
this.tui.requestRender();
|
|
147
|
+
void this.refreshData(force)
|
|
148
|
+
.then((snapshot) => {
|
|
149
|
+
if (this.closed) return;
|
|
150
|
+
const selectedId = this.snapshot.accounts[this.selected]?.id;
|
|
151
|
+
this.snapshot = snapshot;
|
|
152
|
+
const nextIndex = selectedId
|
|
153
|
+
? snapshot.accounts.findIndex((account) => account.id === selectedId)
|
|
154
|
+
: -1;
|
|
155
|
+
this.selected = nextIndex >= 0
|
|
156
|
+
? nextIndex
|
|
157
|
+
: Math.min(this.selected, Math.max(0, snapshot.accounts.length - 1));
|
|
158
|
+
})
|
|
159
|
+
.catch((error) => {
|
|
160
|
+
if (this.closed) return;
|
|
161
|
+
this.snapshot = {
|
|
162
|
+
...this.snapshot,
|
|
163
|
+
warning: `Refresh failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
164
|
+
};
|
|
165
|
+
})
|
|
166
|
+
.finally(() => {
|
|
167
|
+
if (this.closed) return;
|
|
168
|
+
this.refreshing = false;
|
|
169
|
+
this.tui.requestRender();
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
dispose() {
|
|
174
|
+
this.closed = true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
invalidate() {}
|
|
178
|
+
|
|
179
|
+
handleInput(data: string) {
|
|
180
|
+
const accounts = this.snapshot.accounts;
|
|
181
|
+
if (this.keys.matches(data, "tui.select.cancel")) {
|
|
182
|
+
this.closed = true;
|
|
183
|
+
this.done();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (this.keys.matches(data, "tui.select.up") || data === "k") {
|
|
187
|
+
if (accounts.length) {
|
|
188
|
+
this.selected = (this.selected - 1 + accounts.length) % accounts.length;
|
|
189
|
+
}
|
|
190
|
+
} else if (this.keys.matches(data, "tui.select.down") || data === "j") {
|
|
191
|
+
if (accounts.length) this.selected = (this.selected + 1) % accounts.length;
|
|
192
|
+
} else if (data === "r") {
|
|
193
|
+
this.startRefresh(true);
|
|
194
|
+
}
|
|
195
|
+
this.tui.requestRender();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
render(width: number) {
|
|
199
|
+
const rows = this.tui.terminal.rows || 30;
|
|
200
|
+
const bodyHeight = Math.max(8, rows - 7);
|
|
201
|
+
const accounts = this.snapshot.accounts;
|
|
202
|
+
this.selected = Math.min(this.selected, Math.max(0, accounts.length - 1));
|
|
203
|
+
const selected = accounts[this.selected];
|
|
204
|
+
const ready = `${this.snapshot.active}/${this.snapshot.configured} active`;
|
|
205
|
+
const headerRight = this.refreshing
|
|
206
|
+
? this.theme.fg("warning", "refreshing…")
|
|
207
|
+
: this.snapshot.warning
|
|
208
|
+
? this.theme.fg("warning", oneLine(this.snapshot.warning))
|
|
209
|
+
: this.theme.fg("muted", `${ready} · ${this.snapshot.modelCount} models · Droid ${this.snapshot.version}`);
|
|
210
|
+
const title = ` ${this.theme.fg("accent", this.theme.bold("◆ FACTORY"))} ${this.theme.fg("dim", `· ${this.snapshot.authentication}`)}`;
|
|
211
|
+
const lines = width >= 60
|
|
212
|
+
? [joinSides(title, `${headerRight} `, width)]
|
|
213
|
+
: [truncateToWidth(title, width, ""), truncateToWidth(` ${headerRight}`, width, "")];
|
|
214
|
+
lines.push(
|
|
215
|
+
frameTop(this.theme, width, `ACCOUNTS ${accounts.length} · USAGE CONSUMED`),
|
|
216
|
+
);
|
|
217
|
+
const innerWidth = Math.max(1, width - 2);
|
|
218
|
+
if (width >= 104 && selected) {
|
|
219
|
+
const leftWidth = Math.max(46, Math.floor((innerWidth - 1) * 0.48));
|
|
220
|
+
const rightWidth = Math.max(24, innerWidth - leftWidth - 1);
|
|
221
|
+
const list = this.renderAccounts(leftWidth, bodyHeight, true);
|
|
222
|
+
const detail = this.renderDetail(selected, rightWidth, bodyHeight);
|
|
223
|
+
for (let row = 0; row < bodyHeight; row++) {
|
|
224
|
+
lines.push(
|
|
225
|
+
this.theme.fg("border", "│") +
|
|
226
|
+
padLine(list[row] ?? "", leftWidth) +
|
|
227
|
+
this.theme.fg("borderMuted", "│") +
|
|
228
|
+
padLine(detail[row] ?? "", rightWidth) +
|
|
229
|
+
this.theme.fg("border", "│"),
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
const list = this.renderAccounts(innerWidth, bodyHeight, false);
|
|
234
|
+
for (let row = 0; row < bodyHeight; row++) {
|
|
235
|
+
lines.push(
|
|
236
|
+
this.theme.fg("border", "│") +
|
|
237
|
+
padLine(list[row] ?? "", innerWidth) +
|
|
238
|
+
this.theme.fg("border", "│"),
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
lines.push(frameBottom(this.theme, width));
|
|
243
|
+
lines.push(
|
|
244
|
+
truncateToWidth(
|
|
245
|
+
`${this.theme.fg("accent", " ↑↓ / j k")} ${this.theme.fg("dim", "select")} ${this.theme.fg("accent", "r")} ${this.theme.fg("dim", "refresh all")} ${this.theme.fg("accent", "esc")} ${this.theme.fg("dim", "close")}`,
|
|
246
|
+
width,
|
|
247
|
+
"",
|
|
248
|
+
),
|
|
249
|
+
);
|
|
250
|
+
return lines.map((line) => truncateToWidth(line, width, ""));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private renderAccounts(width: number, height: number, compact: boolean) {
|
|
254
|
+
const accounts = this.snapshot.accounts;
|
|
255
|
+
if (!accounts.length) {
|
|
256
|
+
return [
|
|
257
|
+
this.theme.fg("muted", " No Factory credentials configured."),
|
|
258
|
+
this.theme.fg("dim", " Run /login factory to configure an account or API-key file."),
|
|
259
|
+
];
|
|
260
|
+
}
|
|
261
|
+
const rowsPerAccount = compact ? 1 : 4;
|
|
262
|
+
const visibleCount = Math.max(1, Math.floor(height / rowsPerAccount));
|
|
263
|
+
const start = Math.min(
|
|
264
|
+
Math.max(0, this.selected - Math.floor(visibleCount / 2)),
|
|
265
|
+
Math.max(0, accounts.length - visibleCount),
|
|
266
|
+
);
|
|
267
|
+
const visible = accounts.slice(start, start + visibleCount);
|
|
268
|
+
const lines: string[] = [];
|
|
269
|
+
for (const [offset, account] of visible.entries()) {
|
|
270
|
+
const index = start + offset;
|
|
271
|
+
const selected = index === this.selected;
|
|
272
|
+
const state = accountState(account);
|
|
273
|
+
const marker = selected ? this.theme.fg("accent", "◆") : " ";
|
|
274
|
+
const title = this.theme.fg(selected ? "accent" : "text", oneLine(account.label));
|
|
275
|
+
const status = stateLabel(this.theme, stateColor(state), state);
|
|
276
|
+
const first = joinSides(` ${marker} ${title}`, `${status} `, width);
|
|
277
|
+
lines.push(selected ? this.theme.bg("selectedBg", padLine(first, width)) : first);
|
|
278
|
+
if (!compact) {
|
|
279
|
+
lines.push(` ${this.theme.fg("muted", compactPool("Standard", account.record?.limits?.standard, account.record?.fetchedAt))}`);
|
|
280
|
+
lines.push(` ${this.theme.fg("muted", compactPool("Core", account.record?.limits?.core, account.record?.fetchedAt))}`);
|
|
281
|
+
lines.push("");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (start > 0 && lines.length) lines[0] = this.theme.fg("dim", ` ↑ ${start} more`);
|
|
285
|
+
const below = accounts.length - start - visible.length;
|
|
286
|
+
if (below > 0 && lines.length) lines[lines.length - 1] = this.theme.fg("dim", ` ↓ ${below} more`);
|
|
287
|
+
return lines.slice(0, height);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private renderDetail(account: FactoryDashboardAccount, width: number, height: number) {
|
|
291
|
+
const state = accountState(account);
|
|
292
|
+
const lines = [
|
|
293
|
+
` ${stateLabel(this.theme, stateColor(state), state)} ${this.theme.fg("accent", this.theme.bold(oneLine(account.label)))}`,
|
|
294
|
+
account.record
|
|
295
|
+
? ` ${this.theme.fg("dim", `updated ${Math.max(0, Math.floor((Date.now() - account.record.fetchedAt) / 60_000))}m ago`)}`
|
|
296
|
+
: ` ${this.theme.fg("dim", "usage has not been refreshed")}`,
|
|
297
|
+
];
|
|
298
|
+
if (account.cooldownSeconds) {
|
|
299
|
+
lines.push(` ${this.theme.fg("warning", `rotation cooldown ${remainingText({ secondsRemaining: account.cooldownSeconds })}`)}`);
|
|
300
|
+
}
|
|
301
|
+
if (account.record?.error) {
|
|
302
|
+
lines.push(` ${this.theme.fg("error", oneLine(account.record.error))}`);
|
|
303
|
+
}
|
|
304
|
+
lines.push("", ...this.renderPool("Standard", account.record?.limits?.standard, width, account.record?.fetchedAt));
|
|
305
|
+
lines.push("", ...this.renderPool("Droid Core", account.record?.limits?.core, width, account.record?.fetchedAt));
|
|
306
|
+
if (account.lastUsedAt) {
|
|
307
|
+
lines.push("", ` ${this.theme.fg("muted", "LAST USED")} ${this.theme.fg("text", account.lastUsedAt)}`);
|
|
308
|
+
}
|
|
309
|
+
return lines.slice(0, height);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private renderPool(
|
|
313
|
+
name: string,
|
|
314
|
+
limits: FactoryPoolLimits | undefined,
|
|
315
|
+
width: number,
|
|
316
|
+
observedAt = Date.now(),
|
|
317
|
+
) {
|
|
318
|
+
const lines = [` ${this.theme.fg("accent", this.theme.bold(`${name.toUpperCase()} · CONSUMED`))}`];
|
|
319
|
+
if (!limits) return [...lines, ` ${this.theme.fg("muted", "Unavailable")}`];
|
|
320
|
+
const exhausted = exhaustedBucket(limits, Date.now(), observedAt);
|
|
321
|
+
const windows = [
|
|
322
|
+
["MONTHLY", limits.monthly, false],
|
|
323
|
+
["WEEKLY", limits.weekly, exhausted === "monthly"],
|
|
324
|
+
["5-HOUR", limits.fiveHour, exhausted === "monthly" || exhausted === "weekly"],
|
|
325
|
+
] as const;
|
|
326
|
+
for (const [label, bucket, suppressed] of windows) {
|
|
327
|
+
const isActive = bucketActive(bucket, Date.now(), observedAt) && !suppressed;
|
|
328
|
+
const state = bucket.usedPercent >= 100 && isActive
|
|
329
|
+
? "error"
|
|
330
|
+
: bucket.usedPercent >= 85 && isActive
|
|
331
|
+
? "warning"
|
|
332
|
+
: "active";
|
|
333
|
+
const value = isActive ? bucket.usedPercent : 0;
|
|
334
|
+
lines.push(` ${this.theme.fg("muted", label.padEnd(7))} ${meter(this.theme, value, 100, Math.max(12, width - 21), isActive ? state : "muted")} ${this.theme.fg("dim", suppressed ? "parent exhausted" : remainingText(bucket, Date.now(), observedAt))}`);
|
|
335
|
+
}
|
|
336
|
+
return lines;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export async function openFactoryDashboard(
|
|
341
|
+
ctx: ExtensionContext,
|
|
342
|
+
initial: FactoryDashboardSnapshot,
|
|
343
|
+
refresh: (force: boolean) => Promise<FactoryDashboardSnapshot>,
|
|
344
|
+
) {
|
|
345
|
+
await ctx.ui.custom<void>(
|
|
346
|
+
(tui, theme, keys, done) => {
|
|
347
|
+
const dashboard = new FactoryDashboard(
|
|
348
|
+
tui,
|
|
349
|
+
theme,
|
|
350
|
+
keys,
|
|
351
|
+
initial,
|
|
352
|
+
refresh,
|
|
353
|
+
() => done(undefined),
|
|
354
|
+
);
|
|
355
|
+
queueMicrotask(() => dashboard.startRefresh(false));
|
|
356
|
+
return dashboard;
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
overlay: true,
|
|
360
|
+
overlayOptions: { anchor: "center", width: "100%", maxHeight: "100%" },
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync } from "fs";
|
|
3
|
+
import { delimiter, dirname, isAbsolute, join, resolve } from "path";
|
|
4
|
+
import { DEFAULT_DROID_BINARY, DROID_CACHE_PATH, FALLBACK_DROID_VERSION } from "./constants.ts";
|
|
5
|
+
|
|
6
|
+
type DroidCache = {
|
|
7
|
+
binaryPath: string;
|
|
8
|
+
binaryMtimeMs: number;
|
|
9
|
+
version: string;
|
|
10
|
+
execHelp: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function droidPath() {
|
|
14
|
+
const configured = process.env.FACTORY_DROID_BINARY || process.env.DROID_BINARY || DEFAULT_DROID_BINARY;
|
|
15
|
+
const candidates = isAbsolute(configured) || configured.includes("/") || configured.includes("\\")
|
|
16
|
+
? [resolve(configured)]
|
|
17
|
+
: (process.env.PATH || "").split(delimiter).filter(Boolean).map((dir) => join(dir, configured));
|
|
18
|
+
for (const candidate of candidates) {
|
|
19
|
+
try {
|
|
20
|
+
accessSync(candidate, constants.X_OK);
|
|
21
|
+
return realpathSync(candidate);
|
|
22
|
+
} catch {
|
|
23
|
+
// Keep searching PATH; callers retain the configured fallback below.
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return configured;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function binaryMtimeMs(path: string) {
|
|
30
|
+
try {
|
|
31
|
+
return statSync(path).mtimeMs;
|
|
32
|
+
} catch {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readCache(): DroidCache | null {
|
|
38
|
+
try {
|
|
39
|
+
if (!existsSync(DROID_CACHE_PATH)) return null;
|
|
40
|
+
const cache = JSON.parse(readFileSync(DROID_CACHE_PATH, "utf8")) as DroidCache;
|
|
41
|
+
const path = droidPath();
|
|
42
|
+
if (cache.binaryPath !== path || cache.binaryMtimeMs !== binaryMtimeMs(path)) return null;
|
|
43
|
+
return cache;
|
|
44
|
+
} catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function writeCache(cache: DroidCache) {
|
|
50
|
+
try {
|
|
51
|
+
mkdirSync(dirname(DROID_CACHE_PATH), { recursive: true, mode: 0o700 });
|
|
52
|
+
writeFileSync(DROID_CACHE_PATH, JSON.stringify(cache), { mode: 0o600 });
|
|
53
|
+
} catch {
|
|
54
|
+
// Cache is best-effort; never block Pi startup on it.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function refreshCache(): DroidCache | null {
|
|
59
|
+
const path = droidPath();
|
|
60
|
+
try {
|
|
61
|
+
const version = execFileSync(path, ["--version"], { encoding: "utf8", timeout: 5000 }).trim() || FALLBACK_DROID_VERSION;
|
|
62
|
+
const execHelp = execFileSync(path, ["exec", "--help"], { encoding: "utf8", timeout: 10_000 });
|
|
63
|
+
const cache = { binaryPath: path, binaryMtimeMs: binaryMtimeMs(path), version, execHelp };
|
|
64
|
+
writeCache(cache);
|
|
65
|
+
return cache;
|
|
66
|
+
} catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getCache() {
|
|
72
|
+
// Do not shell out to Droid during normal Pi startup/reload. It adds seconds.
|
|
73
|
+
// Refresh explicitly with FACTORY_DROID_REFRESH=1 pi --list-models, then /reload.
|
|
74
|
+
if (process.env.FACTORY_DROID_REFRESH === "1") return refreshCache() || readCache();
|
|
75
|
+
return readCache();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function droidVersion() {
|
|
79
|
+
return getCache()?.version || FALLBACK_DROID_VERSION;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function droidExecHelp() {
|
|
83
|
+
return getCache()?.execHelp || "";
|
|
84
|
+
}
|