pi-jar 0.1.8 → 0.2.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 +26 -13
- package/docs/DISCUSSION_BROKER_PLAN.md +333 -0
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/LONG_SESSION_PERFORMANCE_PLAN.md +731 -0
- package/docs/SUBAGENT-TOOLS.md +2 -0
- package/extensions/index.ts +304 -76
- package/package.json +2 -2
- package/src/activity-view.ts +10 -6
- package/src/advisor.ts +226 -73
- package/src/composer.ts +21 -4
- package/src/context-diet.ts +94 -0
- package/src/delegate-worktree.ts +33 -13
- package/src/delegate.ts +736 -429
- package/src/democracy.ts +6 -4
- package/src/discussion-hub.ts +418 -0
- package/src/discussion.ts +419 -187
- package/src/footer-settings.ts +3 -29
- package/src/model-roles.ts +29 -9
- package/src/perf.ts +419 -0
- package/src/profile-wizard.ts +61 -0
- package/src/profiles.ts +148 -0
- package/src/quota.ts +65 -25
- package/src/settings-ui.ts +46 -14
- package/src/settings.ts +6 -13
- package/src/shell-notifications.ts +33 -6
- package/src/shells.ts +138 -29
- package/src/side-model.ts +3 -1
- package/src/welcome.ts +6 -1
package/src/shells.ts
CHANGED
|
@@ -13,9 +13,19 @@ const MAX_KEPT_SHELLS = 20;
|
|
|
13
13
|
const MAX_LINES = 2000;
|
|
14
14
|
const LINE_PRUNE_BATCH = 128;
|
|
15
15
|
const MAX_LINE_CHARS = 2000;
|
|
16
|
+
/** Per-job cap. Alone it would let every retained job pin ~1 MiB of logs. */
|
|
16
17
|
const MAX_RETAINED_CHARS = 1024 * 1024;
|
|
18
|
+
/** All jobs together. Floors below (8 live tails + 20 compact tails) stay well under it. */
|
|
19
|
+
const DEFAULT_BUDGET_CHARS = 4 * 1024 * 1024;
|
|
20
|
+
/** Budget pressure never trims a live job below this, so services stay observable. */
|
|
21
|
+
const DEFAULT_LIVE_TAIL_CHARS = 64 * 1024;
|
|
17
22
|
const MAX_TOOL_OUTPUT_CHARS = 32 * 1024;
|
|
23
|
+
/** A surfaced finished job keeps exactly what one `output` call can return. */
|
|
24
|
+
const COMPACT_TAIL_CHARS = MAX_TOOL_OUTPUT_CHARS;
|
|
25
|
+
const MAX_OUTPUT_LINES = 400;
|
|
18
26
|
const KILL_GRACE_MS = 3000;
|
|
27
|
+
/** Trim 1/16 below a cap so a chatty job does not re-trim on every following line. */
|
|
28
|
+
const slack = (chars: number) => chars - (chars >> 4);
|
|
19
29
|
|
|
20
30
|
export type ShellStatus = "running" | "exited" | "killed" | "failed";
|
|
21
31
|
export interface ShellJob {
|
|
@@ -42,6 +52,19 @@ export interface ShellJob {
|
|
|
42
52
|
}
|
|
43
53
|
export type ShellEvent = { kind: "match" | "exit"; job: ShellJob };
|
|
44
54
|
export interface StartOptions { command: string; cwd: string; name?: string; watch?: string; notify?: boolean; purpose?: "task" | "service" }
|
|
55
|
+
export interface ShellManagerOptions { budgetChars?: number; liveTailChars?: number }
|
|
56
|
+
export interface ShellStats {
|
|
57
|
+
/** Processes still alive, including killed jobs in their grace period and draining trees. */
|
|
58
|
+
live: number;
|
|
59
|
+
finished: number;
|
|
60
|
+
retainedChars: number;
|
|
61
|
+
budgetChars: number;
|
|
62
|
+
/** Live jobs marked purpose: service — expected long-lived processes, not leftovers. */
|
|
63
|
+
services: number;
|
|
64
|
+
oldestLiveStartedAt?: number;
|
|
65
|
+
/** `pid` is reported only while the process group is alive; a finished pid may be reused. */
|
|
66
|
+
jobs: Array<{ id: string; name: string; status: ShellStatus; purpose: "task" | "service"; pid?: number; chars: number }>;
|
|
67
|
+
}
|
|
45
68
|
|
|
46
69
|
const ANSI = /\x1b\][^\x07]*(?:\x07|\x1b\\)?|\x1b\[[0-?]*[ -/]*[@-~]|\x1b./g;
|
|
47
70
|
|
|
@@ -50,23 +73,33 @@ interface ShellEntry {
|
|
|
50
73
|
child?: ChildProcess;
|
|
51
74
|
pattern?: RegExp;
|
|
52
75
|
partial: string;
|
|
76
|
+
/** Retained line characters, maintained incrementally with the manager total. */
|
|
77
|
+
chars: number;
|
|
53
78
|
timer?: ReturnType<typeof setTimeout>;
|
|
54
79
|
closeTimer?: ReturnType<typeof setTimeout>;
|
|
55
80
|
finished?: boolean;
|
|
56
81
|
}
|
|
57
82
|
|
|
58
|
-
/** Long-running shell commands with bounded output and optional pattern watches.
|
|
83
|
+
/** Long-running shell commands with bounded output and optional pattern watches. Retained logs
|
|
84
|
+
* are bounded per job and globally; log pressure trims text, never processes. */
|
|
59
85
|
export class ShellManager {
|
|
60
86
|
private jobs = new Map<string, ShellEntry>();
|
|
61
87
|
private next = 1;
|
|
62
88
|
private pending = new Map<string, ShellEvent>();
|
|
63
89
|
private listeners = new Set<() => void>();
|
|
64
90
|
private disposed = false;
|
|
65
|
-
private
|
|
91
|
+
private retained = 0;
|
|
92
|
+
private readonly budgetChars: number;
|
|
93
|
+
private readonly liveTailChars: number;
|
|
66
94
|
private readonly onEvent: (event: ShellEvent) => void;
|
|
67
95
|
private readonly spawnShell: typeof spawn;
|
|
68
96
|
onChange?: () => void;
|
|
69
|
-
constructor(onEvent: (event: ShellEvent) => void, spawnShell: typeof spawn = spawn
|
|
97
|
+
constructor(onEvent: (event: ShellEvent) => void, spawnShell: typeof spawn = spawn, options: ShellManagerOptions = {}) {
|
|
98
|
+
this.onEvent = onEvent;
|
|
99
|
+
this.spawnShell = spawnShell;
|
|
100
|
+
this.budgetChars = Math.max(1, Math.floor(options.budgetChars ?? DEFAULT_BUDGET_CHARS));
|
|
101
|
+
this.liveTailChars = Math.max(0, Math.floor(options.liveTailChars ?? DEFAULT_LIVE_TAIL_CHARS));
|
|
102
|
+
}
|
|
70
103
|
|
|
71
104
|
list(): ShellJob[] { return [...this.jobs.values()].map(({ job }) => ({ ...job, lines: [...job.lines] })); }
|
|
72
105
|
/** Read by the footer on every render: one shallow copy per job, never the output lines. */
|
|
@@ -77,12 +110,23 @@ export class ShellManager {
|
|
|
77
110
|
});
|
|
78
111
|
}
|
|
79
112
|
get(id: string): ShellJob | undefined { const entry = this.jobs.get(id); return entry && { ...entry.job, lines: [...entry.job.lines] }; }
|
|
80
|
-
|
|
81
|
-
|
|
113
|
+
/** The agent saw these results: finished jobs drop everything but their compact summary. */
|
|
114
|
+
acknowledge(ids: readonly string[]): void {
|
|
115
|
+
for (const id of ids) { this.pending.delete(id); this.compact(id); }
|
|
116
|
+
}
|
|
117
|
+
/** Taken events are being delivered to the agent, which surfaces their results. */
|
|
118
|
+
takeNotifications(): ShellEvent[] {
|
|
119
|
+
const events = [...this.pending.values()];
|
|
120
|
+
this.pending.clear();
|
|
121
|
+
for (const event of events) this.compact(event.job.id);
|
|
122
|
+
return events;
|
|
123
|
+
}
|
|
82
124
|
restoreNotifications(events: readonly ShellEvent[]): void {
|
|
83
125
|
for (const event of events) if (this.jobs.has(event.job.id) && !this.pending.has(event.job.id)) this.pending.set(event.job.id, event);
|
|
84
126
|
}
|
|
85
127
|
pendingNotifications(): number { return this.pending.size; }
|
|
128
|
+
/** Non-consuming status snapshots for immediate UI feedback; never copy or expose log lines. */
|
|
129
|
+
notificationSnapshot(): ShellEvent[] { return [...this.pending.values()].map(event => ({ ...event, job: { ...event.job, lines: [] } })); }
|
|
86
130
|
verificationPending(): Array<Omit<ShellJob, "lines">> {
|
|
87
131
|
return [...this.jobs.values()].filter(entry => !entry.finished && entry.job.purpose !== "service").map(({ job }) => {
|
|
88
132
|
const { lines: _lines, ...summary } = job;
|
|
@@ -135,9 +179,8 @@ export class ShellManager {
|
|
|
135
179
|
const id = "s" + this.next++;
|
|
136
180
|
const job: ShellJob = { id, name: cleanText(options.name || command, 40), command, cwd: options.cwd, startedAt: Date.now(), status: "running",
|
|
137
181
|
notify: options.notify ?? true, complete: false, purpose: options.purpose ?? (options.watch ? "service" : "task"), lines: [], dropped: 0, ...(options.watch ? { watch: options.watch } : {}) };
|
|
138
|
-
const entry: ShellEntry = { job, partial: "", ...(pattern ? { pattern } : {}) };
|
|
182
|
+
const entry: ShellEntry = { job, partial: "", chars: 0, ...(pattern ? { pattern } : {}) };
|
|
139
183
|
this.jobs.set(id, entry);
|
|
140
|
-
this.lineChars.set(id, 0);
|
|
141
184
|
// Own process group so kill() stops the whole tree (dev servers spawn children).
|
|
142
185
|
const child = this.spawnShell("/bin/sh", ["-c", command], { cwd: options.cwd, detached: process.platform !== "win32", stdio: ["ignore", "pipe", "pipe"], env: process.env });
|
|
143
186
|
entry.child = child;
|
|
@@ -184,34 +227,67 @@ export class ShellManager {
|
|
|
184
227
|
return { ...job, lines: [] };
|
|
185
228
|
}
|
|
186
229
|
|
|
187
|
-
private receive(entry:
|
|
230
|
+
private receive(entry: ShellEntry, text: string): void {
|
|
188
231
|
const parts = (entry.partial + text.replace(/\r\n/g, "\n")).split("\n");
|
|
189
232
|
entry.partial = parts.pop() ?? "";
|
|
190
233
|
if (entry.partial.length > MAX_LINE_CHARS) { this.push(entry, entry.partial); entry.partial = ""; }
|
|
191
234
|
for (const line of parts) this.push(entry, line);
|
|
192
235
|
}
|
|
193
236
|
|
|
194
|
-
private push(entry:
|
|
237
|
+
private push(entry: ShellEntry, raw: string): void {
|
|
238
|
+
// Disposed jobs are forgotten while their trees wind down; late output must not be counted.
|
|
239
|
+
if (this.disposed) return;
|
|
195
240
|
const line = raw.replace(ANSI, "").replace(/\r/g, "").slice(0, MAX_LINE_CHARS);
|
|
196
241
|
const { job } = entry;
|
|
197
242
|
job.lines.push(line);
|
|
198
|
-
|
|
243
|
+
entry.chars += line.length;
|
|
244
|
+
this.retained += line.length;
|
|
199
245
|
// Bound both line count and retained characters. A shell that prints very wide lines should
|
|
200
246
|
// not quietly reserve several MiB forever just because it has not reached MAX_LINES yet.
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
let removedChars = 0;
|
|
204
|
-
for (const item of removed) removedChars += item.length;
|
|
205
|
-
this.lineChars.set(job.id, Math.max(0, (this.lineChars.get(job.id) ?? 0) - removedChars));
|
|
206
|
-
job.dropped += removed.length;
|
|
207
|
-
if (!removed.length) break;
|
|
208
|
-
}
|
|
247
|
+
if (job.lines.length > MAX_LINES + LINE_PRUNE_BATCH || entry.chars > MAX_RETAINED_CHARS) this.trim(entry, slack(MAX_RETAINED_CHARS), MAX_LINES);
|
|
248
|
+
if (this.retained > this.budgetChars) this.enforceBudget();
|
|
209
249
|
if (entry.pattern && !job.matched && entry.pattern.test(line)) {
|
|
210
250
|
job.matched = line;
|
|
211
251
|
this.emitEvent("match", job);
|
|
212
252
|
}
|
|
213
253
|
}
|
|
214
254
|
|
|
255
|
+
/** Drop oldest lines until at most `chars` characters and `lines` lines remain. One splice. */
|
|
256
|
+
private trim(entry: ShellEntry, chars: number, lines = Infinity): void {
|
|
257
|
+
const buffer = entry.job.lines;
|
|
258
|
+
let count = 0;
|
|
259
|
+
let released = 0;
|
|
260
|
+
while (count < buffer.length && (entry.chars - released > chars || buffer.length - count > lines)) released += buffer[count++]!.length;
|
|
261
|
+
if (!count) return;
|
|
262
|
+
buffer.splice(0, count);
|
|
263
|
+
entry.chars -= released;
|
|
264
|
+
this.retained -= released;
|
|
265
|
+
entry.job.dropped += count;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Over budget: shrink oldest finished jobs to their compact tail, then oldest live jobs to
|
|
269
|
+
* their protected tail, then empty finished jobs. Jobs (≤ MAX_KEPT_SHELLS) iterate oldest
|
|
270
|
+
* first; the slack target keeps this off the per-line path. Processes are never touched. */
|
|
271
|
+
private enforceBudget(): void {
|
|
272
|
+
const target = slack(this.budgetChars);
|
|
273
|
+
for (const [floor, finished] of [[COMPACT_TAIL_CHARS, true], [this.liveTailChars, false], [0, true]] as const) {
|
|
274
|
+
for (const entry of this.jobs.values()) {
|
|
275
|
+
if (this.retained <= target) return;
|
|
276
|
+
if (!!entry.finished === finished) this.trim(entry, Math.max(floor, entry.chars - (this.retained - target)));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Keep exit status, command, timestamps, watch match and a bounded tail once surfaced. */
|
|
282
|
+
private compact(id: string): void {
|
|
283
|
+
const entry = this.jobs.get(id);
|
|
284
|
+
if (!entry?.finished) return;
|
|
285
|
+
this.trim(entry, COMPACT_TAIL_CHARS, MAX_OUTPUT_LINES);
|
|
286
|
+
// A finished job never reads its stream or matches again; release the process handle.
|
|
287
|
+
delete entry.child;
|
|
288
|
+
delete entry.pattern;
|
|
289
|
+
}
|
|
290
|
+
|
|
215
291
|
private finish(entry: ShellEntry): void {
|
|
216
292
|
if (entry.finished) return;
|
|
217
293
|
entry.finished = true;
|
|
@@ -231,12 +307,34 @@ export class ShellManager {
|
|
|
231
307
|
output(id: string, count = 40): string[] {
|
|
232
308
|
const entry = this.jobs.get(id);
|
|
233
309
|
if (!entry) throw new Error("no shell " + id);
|
|
234
|
-
const limit = Math.max(1, Math.min(
|
|
310
|
+
const limit = Math.max(1, Math.min(MAX_OUTPUT_LINES, Math.floor(count)));
|
|
235
311
|
if (!entry.partial) return entry.job.lines.slice(-limit);
|
|
236
312
|
if (limit === 1) return [entry.partial];
|
|
237
313
|
return [...entry.job.lines.slice(-(limit - 1)), entry.partial];
|
|
238
314
|
}
|
|
239
315
|
|
|
316
|
+
/** Diagnostics snapshot, computed on demand in one pass over ≤ MAX_KEPT_SHELLS jobs. */
|
|
317
|
+
stats(): ShellStats {
|
|
318
|
+
let live = 0;
|
|
319
|
+
let services = 0;
|
|
320
|
+
let partials = 0;
|
|
321
|
+
let oldestLiveStartedAt: number | undefined;
|
|
322
|
+
const jobs: ShellStats["jobs"] = [];
|
|
323
|
+
for (const entry of this.jobs.values()) {
|
|
324
|
+
const { job } = entry;
|
|
325
|
+
const purpose = job.purpose ?? "task";
|
|
326
|
+
partials += entry.partial.length;
|
|
327
|
+
if (!entry.finished) {
|
|
328
|
+
live++;
|
|
329
|
+
if (purpose === "service") services++;
|
|
330
|
+
if (oldestLiveStartedAt === undefined || job.startedAt < oldestLiveStartedAt) oldestLiveStartedAt = job.startedAt;
|
|
331
|
+
}
|
|
332
|
+
jobs.push({ id: job.id, name: job.name, status: job.status, purpose, chars: entry.chars + entry.partial.length, ...(!entry.finished && job.pid !== undefined ? { pid: job.pid } : {}) });
|
|
333
|
+
}
|
|
334
|
+
return { live, finished: this.jobs.size - live, retainedChars: this.retained + partials, budgetChars: this.budgetChars, services,
|
|
335
|
+
...(oldestLiveStartedAt !== undefined ? { oldestLiveStartedAt } : {}), jobs };
|
|
336
|
+
}
|
|
337
|
+
|
|
240
338
|
kill(id: string): boolean {
|
|
241
339
|
const entry = this.jobs.get(id);
|
|
242
340
|
if (!entry) throw new Error("no shell " + id);
|
|
@@ -255,13 +353,17 @@ export class ShellManager {
|
|
|
255
353
|
return true;
|
|
256
354
|
}
|
|
257
355
|
|
|
258
|
-
/** Forget finished jobs beyond the retention limit (oldest first).
|
|
356
|
+
/** Forget finished jobs beyond the retention limit (oldest first). A killed job still in its
|
|
357
|
+
* grace period or a draining tree is alive and stays managed until it finishes. */
|
|
259
358
|
private prune(): void {
|
|
260
|
-
|
|
261
|
-
for (const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
this.
|
|
359
|
+
let excess = this.jobs.size - MAX_KEPT_SHELLS + 1;
|
|
360
|
+
for (const [id, entry] of this.jobs) {
|
|
361
|
+
if (excess <= 0) return;
|
|
362
|
+
if (!entry.finished) continue;
|
|
363
|
+
this.jobs.delete(id);
|
|
364
|
+
this.retained -= entry.chars;
|
|
365
|
+
this.pending.delete(id);
|
|
366
|
+
excess--;
|
|
265
367
|
}
|
|
266
368
|
}
|
|
267
369
|
|
|
@@ -269,9 +371,14 @@ export class ShellManager {
|
|
|
269
371
|
this.disposed = true;
|
|
270
372
|
this.pending.clear();
|
|
271
373
|
for (const listener of [...this.listeners]) listener();
|
|
272
|
-
for (const
|
|
374
|
+
for (const entry of this.jobs.values()) {
|
|
375
|
+
if (entry.finished) continue;
|
|
376
|
+
// Running trees get SIGTERM plus the bounded SIGKILL grace. A draining tree (leader gone,
|
|
377
|
+
// descendants holding pipes) is reclaimed now rather than after its drain timer.
|
|
378
|
+
if (!this.kill(entry.job.id) && entry.job.status !== "killed") this.finish(entry);
|
|
379
|
+
}
|
|
273
380
|
this.jobs.clear();
|
|
274
|
-
this.
|
|
381
|
+
this.retained = 0;
|
|
275
382
|
}
|
|
276
383
|
}
|
|
277
384
|
|
|
@@ -283,7 +390,8 @@ export const elapsed = (span: { startedAt: number; endedAt?: number }, now = Dat
|
|
|
283
390
|
};
|
|
284
391
|
export const shellState = (job: ShellMeta | ShellJob) => job.status === "running" ? "running"
|
|
285
392
|
: job.status === "exited" ? `exited ${job.exitCode ?? "?"}${job.complete === false ? " (draining)" : ""}` : job.status === "failed" ? `failed: ${cleanText(job.error ?? "", 60)}` : "killed";
|
|
286
|
-
|
|
393
|
+
// Services are labelled so a long-lived process reads as intended, not as a forgotten leftover.
|
|
394
|
+
export const describe = (job: ShellMeta | ShellJob) => `${job.id} · ${job.name} · ${shellState(job)} · ${elapsed(job)}${job.purpose === "service" ? " · service" : ""}${job.watch ? ` · watch /${job.watch}/${job.matched ? " matched" : ""}` : ""}`;
|
|
287
395
|
const boundedTail = (lines: readonly string[], limit = MAX_TOOL_OUTPUT_CHARS): string => {
|
|
288
396
|
let size = 0;
|
|
289
397
|
const kept: string[] = [];
|
|
@@ -332,6 +440,7 @@ export function registerShells(pi: ExtensionAPI, shells: () => ShellManager | un
|
|
|
332
440
|
promptGuidelines: [
|
|
333
441
|
"Use jar_shell start for servers, watchers and slow commands; set watch to a regex for the line you are waiting for (e.g. \"listening|ready|error\").",
|
|
334
442
|
"Do not poll: work while commands run, then use wait with explicit ids for relevant checks before the final summary. A bounded wait returns pending jobs honestly; never claim success without their exit codes. Do not wait for services/watchers. output, peek, list and wait acknowledge observed events, preventing stale replay.",
|
|
443
|
+
"Completion events cannot interrupt an in-flight model response. If no useful work remains, call wait for the relevant finite jobs immediately instead of thinking or waiting for an automatic wake-up.",
|
|
335
444
|
"Notifications contain coalesced status, not log dumps. Inspect output only for relevant diagnostics. Mark never-ending jobs purpose: service and kill shells you no longer need."
|
|
336
445
|
],
|
|
337
446
|
parameters: Parameters,
|
|
@@ -347,7 +456,7 @@ export function registerShells(pi: ExtensionAPI, shells: () => ShellManager | un
|
|
|
347
456
|
case "start": {
|
|
348
457
|
const job = manager.start({ command: params.command ?? "", cwd: ctx.cwd, ...(params.name ? { name: params.name } : {}), ...(params.watch ? { watch: params.watch } : {}),
|
|
349
458
|
...(params.notify !== undefined ? { notify: params.notify } : {}), ...(params.purpose ? { purpose: params.purpose } : {}) });
|
|
350
|
-
return reply(`Started ${describe(job)} (pid ${job.pid ?? "?"}). ${job.notify ? "
|
|
459
|
+
return reply(`Started ${describe(job)} (pid ${job.pid ?? "?"}). ${job.notify ? "Completion reaches the model at the next safe turn boundary (or wakes it when idle). If no other useful work remains, use wait now." : "Notifications are off; check it with output."}`);
|
|
351
460
|
}
|
|
352
461
|
case "output": {
|
|
353
462
|
if (!params.id) return reply("output needs an id.");
|
package/src/side-model.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface SideAnswer { text: string; model: string }
|
|
|
30
30
|
* Ask the model assigned to `role` (or the current model) a one-shot question outside the
|
|
31
31
|
* conversation. Tries configured fallbacks in order on failure; cancellation never retries.
|
|
32
32
|
*/
|
|
33
|
-
export async function askRole(ctx: ExtensionContext, roles: ModelRoleManager, usage: SideUsage | undefined, role: string,
|
|
33
|
+
export async function askRole(ctx: ExtensionContext, roles: ModelRoleManager, usage: Pick<SideUsage, "add"> | undefined, role: string,
|
|
34
34
|
systemPrompt: string, prompt: string, signal?: AbortSignal): Promise<SideAnswer> {
|
|
35
35
|
const fallbacks = roles.fallbackSpecs?.(role) ?? [];
|
|
36
36
|
const failures: string[] = [];
|
|
@@ -52,6 +52,8 @@ export async function askRole(ctx: ExtensionContext, roles: ModelRoleManager, us
|
|
|
52
52
|
const result = await ctx.modelRegistry.streamSimple(model,
|
|
53
53
|
{ systemPrompt, messages: [{ role: "user", content: prompt, timestamp: Date.now() }] },
|
|
54
54
|
{ ...(reasoning ? { reasoning } : {}), ...(signal ? { signal } : {}) }).result();
|
|
55
|
+
// Callers with detached work can supply a session-generation-safe usage sink.
|
|
56
|
+
// Returned attempts (including cancelled attempts) still account for actual usage.
|
|
55
57
|
const cost = result.usage?.cost?.total;
|
|
56
58
|
usage?.add({ role, model: name, input: result.usage?.input ?? 0, output: result.usage?.output ?? 0, cacheRead: result.usage?.cacheRead ?? 0,
|
|
57
59
|
cacheWrite: result.usage?.cacheWrite ?? 0, cost: Number.isFinite(cost) ? cost! : 0, at: Date.now() });
|
package/src/welcome.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
1
|
+
import { sliceByColumn, stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
2
2
|
import { FLAME_RAMP, FLAME_WIDTH, flameFrame, rgb, supportsTruecolor } from "./flame.ts";
|
|
3
3
|
import { cleanText } from "./status.ts";
|
|
4
4
|
import { iconSet, withIcon } from "./icons.ts";
|
|
@@ -10,6 +10,8 @@ export interface WelcomeInfo {
|
|
|
10
10
|
model?: string;
|
|
11
11
|
effort?: string;
|
|
12
12
|
activeRole?: string;
|
|
13
|
+
/** Active configuration profile, shown even before a session is named. */
|
|
14
|
+
profile?: string;
|
|
13
15
|
project?: string;
|
|
14
16
|
context?: string;
|
|
15
17
|
cost?: string;
|
|
@@ -189,6 +191,8 @@ function card(width: number, fg: Paint, info: WelcomeInfo): string[] {
|
|
|
189
191
|
cell(center(fg("dim", "— welcome to ") + fg("accent", "pi-jar") + fg("dim", " —"), inner)),
|
|
190
192
|
cell(""),
|
|
191
193
|
divider,
|
|
194
|
+
...wrapTextWithAnsi(cleanText(info.profile ?? "Default", 32) || "Default", Math.max(1, inner - labelWidth))
|
|
195
|
+
.map((line, index) => cell(index ? fg("dim", " ".repeat(labelWidth)) + fg("accent", line) : label("PROFILE", line, "accent"))),
|
|
192
196
|
cell(label("PROJECT", withIcon("folder", info.project || "unavailable") + " · " + git, info.dirty ? "warning" : "muted")),
|
|
193
197
|
cell(label("SESSION", [info.context ?? "ctx ?", quota, info.cost ?? ""].filter(Boolean).join(" · "))),
|
|
194
198
|
divider,
|
|
@@ -262,6 +266,7 @@ function renderWelcome(width: number, frame: number, fg: Paint, info: WelcomeInf
|
|
|
262
266
|
...flame.slice(4).map((line) => fit(sliceByColumn(line, start, Math.min(width, FLAME_WIDTH)))),
|
|
263
267
|
...PI_COMPACT.map((line) => fit(fg("accent", line))),
|
|
264
268
|
fit(fg("accent", "pi-jar")),
|
|
269
|
+
fit(fg("accent", "Profile: " + (cleanText(info.profile ?? "Default", 32) || "Default"))),
|
|
265
270
|
...wrap(cleanText(info.message ?? HOPEFUL_WELCOME_MESSAGES[0], 100), width).map((line) => fit("\x1b[1m" + fg("warning", line) + "\x1b[22m")),
|
|
266
271
|
fit(fg(info.settingsClickable === false ? "dim" : "accent", info.settingsClickable === false ? "ctrl+alt+s settings" : "[ ⚙ Settings ]")),
|
|
267
272
|
""
|