@oh-my-pi/pi-coding-agent 13.11.1 → 13.12.3
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 +95 -0
- package/package.json +7 -7
- package/src/capability/context-file.ts +2 -0
- package/src/capability/extension-module.ts +1 -0
- package/src/capability/hook.ts +1 -0
- package/src/capability/index.ts +21 -10
- package/src/capability/instruction.ts +1 -0
- package/src/capability/mcp.ts +1 -0
- package/src/capability/prompt.ts +1 -0
- package/src/capability/rule.ts +5 -0
- package/src/capability/skill.ts +1 -0
- package/src/capability/slash-command.ts +1 -0
- package/src/capability/tool.ts +1 -0
- package/src/capability/types.ts +10 -0
- package/src/cli/commands/init-xdg.ts +27 -0
- package/src/cli/config-cli.ts +8 -3
- package/src/cli/shell-cli.ts +1 -1
- package/src/commands/config.ts +1 -1
- package/src/config/model-registry.ts +63 -10
- package/src/config/model-resolver.ts +84 -21
- package/src/config/settings-schema.ts +977 -769
- package/src/discovery/helpers.ts +8 -2
- package/src/exec/bash-executor.ts +62 -25
- package/src/extensibility/custom-tools/types.ts +2 -3
- package/src/extensibility/extensions/loader.ts +5 -1
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/extensibility/hooks/types.ts +2 -0
- package/src/extensibility/plugins/loader.ts +23 -5
- package/src/extensibility/plugins/manager.ts +14 -0
- package/src/extensibility/plugins/types.ts +4 -0
- package/src/extensibility/skills.ts +7 -1
- package/src/index.ts +6 -6
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/ipy/kernel.ts +4 -5
- package/src/memories/index.ts +20 -7
- package/src/memories/storage.ts +46 -32
- package/src/modes/components/agent-dashboard.ts +23 -35
- package/src/modes/components/assistant-message.ts +25 -2
- package/src/modes/components/btw-panel.ts +104 -0
- package/src/modes/components/diff.ts +2 -7
- package/src/modes/components/extensions/state-manager.ts +3 -2
- package/src/modes/components/settings-defs.ts +56 -6
- package/src/modes/components/settings-selector.ts +11 -6
- package/src/modes/controllers/btw-controller.ts +193 -0
- package/src/modes/controllers/command-controller.ts +9 -3
- package/src/modes/controllers/event-controller.ts +4 -0
- package/src/modes/controllers/input-controller.ts +10 -1
- package/src/modes/interactive-mode.ts +22 -0
- package/src/modes/prompt-action-autocomplete.ts +17 -3
- package/src/modes/rpc/rpc-client.ts +30 -19
- package/src/modes/theme/theme.ts +28 -36
- package/src/modes/types.ts +4 -0
- package/src/modes/utils/ui-helpers.ts +3 -0
- package/src/patch/diff.ts +9 -1
- package/src/patch/index.ts +56 -9
- package/src/prompts/system/btw-user.md +8 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/sdk.ts +23 -26
- package/src/session/agent-session.ts +65 -37
- package/src/session/blob-store.ts +32 -0
- package/src/session/compaction/compaction.ts +37 -6
- package/src/session/history-storage.ts +2 -2
- package/src/session/session-manager.ts +129 -49
- package/src/slash-commands/builtin-registry.ts +11 -0
- package/src/system-prompt.ts +4 -17
- package/src/task/agents.ts +1 -1
- package/src/task/index.ts +9 -8
- package/src/tools/browser.ts +11 -0
- package/src/tools/output-meta.ts +103 -3
- package/src/tools/path-utils.ts +11 -0
- package/src/utils/title-generator.ts +70 -92
- package/src/utils/tools-manager.ts +1 -1
- package/src/web/scrapers/index.ts +7 -7
- package/src/web/scrapers/utils.ts +1 -0
package/src/memories/storage.ts
CHANGED
|
@@ -34,9 +34,16 @@ export interface GlobalClaim {
|
|
|
34
34
|
|
|
35
35
|
const STAGE1_KIND = "memory_stage1";
|
|
36
36
|
const GLOBAL_KIND = "memory_consolidate_global";
|
|
37
|
-
const GLOBAL_KEY = "global";
|
|
38
37
|
const DEFAULT_RETRY_REMAINING = 3;
|
|
39
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Per-project job key so Phase 2 consolidation is isolated to a single cwd.
|
|
41
|
+
* Previously a single "global" key caused cross-project memory contamination.
|
|
42
|
+
*/
|
|
43
|
+
function globalJobKey(cwd: string): string {
|
|
44
|
+
return `global:${cwd}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
40
47
|
export function openMemoryDb(dbPath: string): Database {
|
|
41
48
|
const db = new Database(dbPath);
|
|
42
49
|
db.exec(`
|
|
@@ -119,11 +126,11 @@ VALUES (?, ?, 'pending', ?, 0, 0)
|
|
|
119
126
|
`).run(STAGE1_KIND, threadId, DEFAULT_RETRY_REMAINING);
|
|
120
127
|
}
|
|
121
128
|
|
|
122
|
-
function ensureGlobalJob(db: Database): void {
|
|
129
|
+
function ensureGlobalJob(db: Database, cwd: string): void {
|
|
123
130
|
db.prepare(`
|
|
124
131
|
INSERT OR IGNORE INTO jobs (kind, job_key, status, retry_remaining, input_watermark, last_success_watermark)
|
|
125
132
|
VALUES (?, ?, 'pending', ?, 0, 0)
|
|
126
|
-
`).run(GLOBAL_KIND,
|
|
133
|
+
`).run(GLOBAL_KIND, globalJobKey(cwd), DEFAULT_RETRY_REMAINING);
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
export function claimStage1Jobs(
|
|
@@ -240,10 +247,11 @@ WHERE kind = ? AND job_key = ?
|
|
|
240
247
|
export function enqueueGlobalWatermark(
|
|
241
248
|
db: Database,
|
|
242
249
|
sourceUpdatedAt: number,
|
|
250
|
+
cwd: string,
|
|
243
251
|
params?: { forceDirtyWhenNotAdvanced?: boolean },
|
|
244
252
|
): void {
|
|
245
253
|
const forceDirtyWhenNotAdvanced = params?.forceDirtyWhenNotAdvanced ?? false;
|
|
246
|
-
ensureGlobalJob(db);
|
|
254
|
+
ensureGlobalJob(db, cwd);
|
|
247
255
|
db.prepare(`
|
|
248
256
|
UPDATE jobs
|
|
249
257
|
SET
|
|
@@ -283,7 +291,7 @@ WHERE kind = ? AND job_key = ?
|
|
|
283
291
|
sourceUpdatedAt,
|
|
284
292
|
forceDirtyWhenNotAdvanced ? 1 : 0,
|
|
285
293
|
GLOBAL_KIND,
|
|
286
|
-
|
|
294
|
+
globalJobKey(cwd),
|
|
287
295
|
);
|
|
288
296
|
}
|
|
289
297
|
|
|
@@ -297,9 +305,10 @@ export function markStage1SucceededWithOutput(
|
|
|
297
305
|
rolloutSummary: string;
|
|
298
306
|
rolloutSlug: string | null;
|
|
299
307
|
nowSec: number;
|
|
308
|
+
cwd: string;
|
|
300
309
|
},
|
|
301
310
|
): boolean {
|
|
302
|
-
const { threadId, ownershipToken, sourceUpdatedAt, rawMemory, rolloutSummary, rolloutSlug, nowSec } = params;
|
|
311
|
+
const { threadId, ownershipToken, sourceUpdatedAt, rawMemory, rolloutSummary, rolloutSlug, nowSec, cwd } = params;
|
|
303
312
|
const tx = db.transaction(() => {
|
|
304
313
|
const matched = db
|
|
305
314
|
.prepare(
|
|
@@ -327,7 +336,7 @@ ON CONFLICT(thread_id) DO UPDATE SET
|
|
|
327
336
|
WHERE excluded.source_updated_at >= stage1_outputs.source_updated_at
|
|
328
337
|
`).run(threadId, sourceUpdatedAt, rawMemory, rolloutSummary, rolloutSlug, nowSec);
|
|
329
338
|
|
|
330
|
-
enqueueGlobalWatermark(db, sourceUpdatedAt, { forceDirtyWhenNotAdvanced: true });
|
|
339
|
+
enqueueGlobalWatermark(db, sourceUpdatedAt, cwd, { forceDirtyWhenNotAdvanced: true });
|
|
331
340
|
return true;
|
|
332
341
|
});
|
|
333
342
|
return tx() as boolean;
|
|
@@ -335,9 +344,9 @@ WHERE excluded.source_updated_at >= stage1_outputs.source_updated_at
|
|
|
335
344
|
|
|
336
345
|
export function markStage1SucceededNoOutput(
|
|
337
346
|
db: Database,
|
|
338
|
-
params: { threadId: string; ownershipToken: string; sourceUpdatedAt: number; nowSec: number },
|
|
347
|
+
params: { threadId: string; ownershipToken: string; sourceUpdatedAt: number; nowSec: number; cwd: string },
|
|
339
348
|
): boolean {
|
|
340
|
-
const { threadId, ownershipToken, sourceUpdatedAt, nowSec } = params;
|
|
349
|
+
const { threadId, ownershipToken, sourceUpdatedAt, nowSec, cwd } = params;
|
|
341
350
|
const tx = db.transaction(() => {
|
|
342
351
|
const matched = db
|
|
343
352
|
.prepare(
|
|
@@ -354,7 +363,7 @@ WHERE kind = ? AND job_key = ? AND ownership_token = ?
|
|
|
354
363
|
`).run(nowSec, STAGE1_KIND, threadId, ownershipToken);
|
|
355
364
|
|
|
356
365
|
db.prepare("DELETE FROM stage1_outputs WHERE thread_id = ?").run(threadId);
|
|
357
|
-
enqueueGlobalWatermark(db, sourceUpdatedAt, { forceDirtyWhenNotAdvanced: true });
|
|
366
|
+
enqueueGlobalWatermark(db, sourceUpdatedAt, cwd, { forceDirtyWhenNotAdvanced: true });
|
|
358
367
|
return true;
|
|
359
368
|
});
|
|
360
369
|
return tx() as boolean;
|
|
@@ -379,15 +388,16 @@ WHERE kind = ? AND job_key = ? AND status = 'running' AND ownership_token = ?
|
|
|
379
388
|
|
|
380
389
|
export function tryClaimGlobalPhase2Job(
|
|
381
390
|
db: Database,
|
|
382
|
-
params: { workerId: string; leaseSeconds: number; nowSec: number },
|
|
391
|
+
params: { workerId: string; leaseSeconds: number; nowSec: number; cwd: string },
|
|
383
392
|
): { kind: "claimed"; claim: GlobalClaim } | { kind: "skipped_not_dirty" } | { kind: "skipped_running" } {
|
|
384
|
-
const { workerId, leaseSeconds, nowSec } = params;
|
|
385
|
-
|
|
393
|
+
const { workerId, leaseSeconds, nowSec, cwd } = params;
|
|
394
|
+
const jobKey = globalJobKey(cwd);
|
|
395
|
+
ensureGlobalJob(db, cwd);
|
|
386
396
|
const pre = db
|
|
387
397
|
.prepare(
|
|
388
398
|
"SELECT status, lease_until, input_watermark, last_success_watermark, retry_at, retry_remaining FROM jobs WHERE kind = ? AND job_key = ?",
|
|
389
399
|
)
|
|
390
|
-
.get(GLOBAL_KIND,
|
|
400
|
+
.get(GLOBAL_KIND, jobKey) as
|
|
391
401
|
| {
|
|
392
402
|
status: string;
|
|
393
403
|
lease_until: number | null;
|
|
@@ -410,11 +420,11 @@ WHERE kind = ? AND job_key = ?
|
|
|
410
420
|
AND retry_remaining > 0
|
|
411
421
|
AND (retry_at IS NULL OR retry_at <= ?)
|
|
412
422
|
`)
|
|
413
|
-
.run(workerId, ownershipToken, nowSec, nowSec + leaseSeconds, GLOBAL_KIND,
|
|
423
|
+
.run(workerId, ownershipToken, nowSec, nowSec + leaseSeconds, GLOBAL_KIND, jobKey, nowSec, nowSec);
|
|
414
424
|
if (Number(claimed.changes ?? 0) > 0) {
|
|
415
425
|
const row = db
|
|
416
426
|
.prepare("SELECT input_watermark FROM jobs WHERE kind = ? AND job_key = ? AND ownership_token = ?")
|
|
417
|
-
.get(GLOBAL_KIND,
|
|
427
|
+
.get(GLOBAL_KIND, jobKey, ownershipToken) as { input_watermark: number | null } | undefined;
|
|
418
428
|
return {
|
|
419
429
|
kind: "claimed",
|
|
420
430
|
claim: {
|
|
@@ -443,7 +453,7 @@ WHERE kind = ? AND job_key = ?
|
|
|
443
453
|
.prepare(
|
|
444
454
|
"SELECT status, lease_until, input_watermark, last_success_watermark, retry_at, retry_remaining FROM jobs WHERE kind = ? AND job_key = ?",
|
|
445
455
|
)
|
|
446
|
-
.get(GLOBAL_KIND,
|
|
456
|
+
.get(GLOBAL_KIND, jobKey) as
|
|
447
457
|
| {
|
|
448
458
|
status: string;
|
|
449
459
|
lease_until: number | null;
|
|
@@ -463,30 +473,34 @@ WHERE kind = ? AND job_key = ?
|
|
|
463
473
|
|
|
464
474
|
export function heartbeatGlobalJob(
|
|
465
475
|
db: Database,
|
|
466
|
-
params: { ownershipToken: string; leaseSeconds: number; nowSec: number },
|
|
476
|
+
params: { ownershipToken: string; leaseSeconds: number; nowSec: number; cwd: string },
|
|
467
477
|
): boolean {
|
|
468
|
-
const { ownershipToken, leaseSeconds, nowSec } = params;
|
|
478
|
+
const { ownershipToken, leaseSeconds, nowSec, cwd } = params;
|
|
469
479
|
const result = db
|
|
470
480
|
.prepare(`
|
|
471
481
|
UPDATE jobs
|
|
472
482
|
SET lease_until = ?
|
|
473
483
|
WHERE kind = ? AND job_key = ? AND status = 'running' AND ownership_token = ?
|
|
474
484
|
`)
|
|
475
|
-
.run(nowSec + leaseSeconds, GLOBAL_KIND,
|
|
485
|
+
.run(nowSec + leaseSeconds, GLOBAL_KIND, globalJobKey(cwd), ownershipToken);
|
|
476
486
|
return Number(result.changes ?? 0) > 0;
|
|
477
487
|
}
|
|
478
488
|
|
|
479
|
-
|
|
489
|
+
// Filter by cwd so each project only consolidates its own thread outputs.
|
|
490
|
+
// Before this filter existed, whichever project ran Phase 2 first got every
|
|
491
|
+
// project's data written into its memory directory (see #369).
|
|
492
|
+
export function listStage1OutputsForGlobal(db: Database, limit: number, cwd: string): Stage1OutputRow[] {
|
|
480
493
|
const rows = db
|
|
481
494
|
.prepare(`
|
|
482
495
|
SELECT o.thread_id, o.source_updated_at, o.raw_memory, o.rollout_summary, o.rollout_slug, o.generated_at, t.cwd
|
|
483
496
|
FROM stage1_outputs o
|
|
484
497
|
LEFT JOIN threads t ON t.id = o.thread_id
|
|
485
|
-
WHERE TRIM(COALESCE(o.raw_memory, '')) != '' OR TRIM(COALESCE(o.rollout_summary, '')) != ''
|
|
498
|
+
WHERE (TRIM(COALESCE(o.raw_memory, '')) != '' OR TRIM(COALESCE(o.rollout_summary, '')) != '')
|
|
499
|
+
AND t.cwd = ?
|
|
486
500
|
ORDER BY o.source_updated_at DESC
|
|
487
501
|
LIMIT ?
|
|
488
502
|
`)
|
|
489
|
-
.all(limit) as Array<{
|
|
503
|
+
.all(cwd, limit) as Array<{
|
|
490
504
|
thread_id: string;
|
|
491
505
|
source_updated_at: number;
|
|
492
506
|
raw_memory: string;
|
|
@@ -508,9 +522,9 @@ LIMIT ?
|
|
|
508
522
|
|
|
509
523
|
export function markGlobalPhase2Succeeded(
|
|
510
524
|
db: Database,
|
|
511
|
-
params: { ownershipToken: string; newWatermark: number; nowSec: number },
|
|
525
|
+
params: { ownershipToken: string; newWatermark: number; nowSec: number; cwd: string },
|
|
512
526
|
): boolean {
|
|
513
|
-
const { ownershipToken, newWatermark, nowSec } = params;
|
|
527
|
+
const { ownershipToken, newWatermark, nowSec, cwd } = params;
|
|
514
528
|
const result = db
|
|
515
529
|
.prepare(`
|
|
516
530
|
UPDATE jobs
|
|
@@ -523,15 +537,15 @@ SET status = 'done', finished_at = ?, lease_until = NULL, retry_at = NULL,
|
|
|
523
537
|
END
|
|
524
538
|
WHERE kind = ? AND job_key = ? AND status = 'running' AND ownership_token = ?
|
|
525
539
|
`)
|
|
526
|
-
.run(nowSec, newWatermark, newWatermark, newWatermark, GLOBAL_KIND,
|
|
540
|
+
.run(nowSec, newWatermark, newWatermark, newWatermark, GLOBAL_KIND, globalJobKey(cwd), ownershipToken);
|
|
527
541
|
return Number(result.changes ?? 0) > 0;
|
|
528
542
|
}
|
|
529
543
|
|
|
530
544
|
export function markGlobalPhase2Failed(
|
|
531
545
|
db: Database,
|
|
532
|
-
params: { ownershipToken: string; retryDelaySeconds: number; reason: string; nowSec: number },
|
|
546
|
+
params: { ownershipToken: string; retryDelaySeconds: number; reason: string; nowSec: number; cwd: string },
|
|
533
547
|
): boolean {
|
|
534
|
-
const { ownershipToken, retryDelaySeconds, reason, nowSec } = params;
|
|
548
|
+
const { ownershipToken, retryDelaySeconds, reason, nowSec, cwd } = params;
|
|
535
549
|
const result = db
|
|
536
550
|
.prepare(`
|
|
537
551
|
UPDATE jobs
|
|
@@ -540,15 +554,15 @@ SET status = 'error', finished_at = ?, lease_until = NULL, retry_at = ?,
|
|
|
540
554
|
last_error = ?
|
|
541
555
|
WHERE kind = ? AND job_key = ? AND status = 'running' AND ownership_token = ?
|
|
542
556
|
`)
|
|
543
|
-
.run(nowSec, nowSec + retryDelaySeconds, reason, GLOBAL_KIND,
|
|
557
|
+
.run(nowSec, nowSec + retryDelaySeconds, reason, GLOBAL_KIND, globalJobKey(cwd), ownershipToken);
|
|
544
558
|
return Number(result.changes ?? 0) > 0;
|
|
545
559
|
}
|
|
546
560
|
|
|
547
561
|
export function markGlobalPhase2FailedUnowned(
|
|
548
562
|
db: Database,
|
|
549
|
-
params: { retryDelaySeconds: number; reason: string; nowSec: number },
|
|
563
|
+
params: { retryDelaySeconds: number; reason: string; nowSec: number; cwd: string },
|
|
550
564
|
): boolean {
|
|
551
|
-
const { retryDelaySeconds, reason, nowSec } = params;
|
|
565
|
+
const { retryDelaySeconds, reason, nowSec, cwd } = params;
|
|
552
566
|
const result = db
|
|
553
567
|
.prepare(`
|
|
554
568
|
UPDATE jobs
|
|
@@ -558,6 +572,6 @@ SET status = 'error', finished_at = ?, lease_until = NULL, retry_at = ?,
|
|
|
558
572
|
WHERE kind = ? AND job_key = ? AND status = 'running'
|
|
559
573
|
AND (ownership_token IS NULL OR lease_until IS NULL OR lease_until <= ?)
|
|
560
574
|
`)
|
|
561
|
-
.run(nowSec, nowSec + retryDelaySeconds, reason, GLOBAL_KIND,
|
|
575
|
+
.run(nowSec, nowSec + retryDelaySeconds, reason, GLOBAL_KIND, globalJobKey(cwd), nowSec);
|
|
562
576
|
return Number(result.changes ?? 0) > 0;
|
|
563
577
|
}
|
|
@@ -35,7 +35,12 @@ import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
|
35
35
|
import { YAML } from "bun";
|
|
36
36
|
import { getConfigDirs } from "../../config";
|
|
37
37
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
38
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
formatModelString,
|
|
40
|
+
resolveAgentModelPatterns,
|
|
41
|
+
resolveConfiguredModelPatterns,
|
|
42
|
+
resolveModelOverride,
|
|
43
|
+
} from "../../config/model-resolver";
|
|
39
44
|
import { renderPromptTemplate } from "../../config/prompt-templates";
|
|
40
45
|
import { Settings } from "../../config/settings";
|
|
41
46
|
import agentCreationArchitectPrompt from "../../prompts/system/agent-creation-architect.md" with { type: "text" };
|
|
@@ -93,21 +98,8 @@ const SOURCE_LABEL: Record<AgentSource, string> = {
|
|
|
93
98
|
|
|
94
99
|
const IDENTIFIER_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+){1,5}$/;
|
|
95
100
|
|
|
96
|
-
function normalizeModelPatterns(value: string | string[] | undefined): string[] {
|
|
97
|
-
if (Array.isArray(value)) {
|
|
98
|
-
return value.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0);
|
|
99
|
-
}
|
|
100
|
-
if (typeof value === "string") {
|
|
101
|
-
const normalized = value.trim();
|
|
102
|
-
if (normalized.length > 0) {
|
|
103
|
-
return [normalized];
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return [];
|
|
107
|
-
}
|
|
108
|
-
|
|
109
101
|
function joinPatterns(patterns: string[]): string {
|
|
110
|
-
if (patterns.length === 0) return "(session
|
|
102
|
+
if (patterns.length === 0) return "(session model)";
|
|
111
103
|
return patterns.join(", ");
|
|
112
104
|
}
|
|
113
105
|
|
|
@@ -398,8 +390,7 @@ export class AgentDashboard extends Container {
|
|
|
398
390
|
const activeTabId = this.#tabs[this.#activeTabIndex]?.id ?? "all";
|
|
399
391
|
const { agents } = await discoverAgents(this.cwd);
|
|
400
392
|
const disabled = new Set((this.#settingsManager?.get("task.disabledAgents") as string[] | undefined) ?? []);
|
|
401
|
-
const overrides =
|
|
402
|
-
(this.#settingsManager?.get("task.agentModelOverrides") as Record<string, string> | undefined) ?? {};
|
|
393
|
+
const overrides = this.#settingsManager?.get("task.agentModelOverrides") ?? {};
|
|
403
394
|
|
|
404
395
|
this.#allAgents = agents
|
|
405
396
|
.slice()
|
|
@@ -622,10 +613,11 @@ export class AgentDashboard extends Container {
|
|
|
622
613
|
await modelRegistry.refresh();
|
|
623
614
|
|
|
624
615
|
const settings = this.#settingsManager ?? undefined;
|
|
625
|
-
const modelPatterns =
|
|
616
|
+
const modelPatterns = resolveConfiguredModelPatterns(
|
|
626
617
|
this.modelContext.activeModelPattern ??
|
|
627
618
|
this.modelContext.defaultModelPattern ??
|
|
628
619
|
settings?.getModelRole("default"),
|
|
620
|
+
settings,
|
|
629
621
|
);
|
|
630
622
|
const { model } = resolveModelOverride(modelPatterns, modelRegistry, settings);
|
|
631
623
|
const fallbackModel = modelRegistry.getAvailable()[0];
|
|
@@ -750,26 +742,22 @@ export class AgentDashboard extends Container {
|
|
|
750
742
|
}
|
|
751
743
|
|
|
752
744
|
#defaultPatternsFor(agent: DashboardAgent): string[] {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
this.modelContext.activeModelPattern?.trim() ||
|
|
760
|
-
this.modelContext.defaultModelPattern?.trim() ||
|
|
761
|
-
this.#settingsManager?.getModelRole("default")?.trim() ||
|
|
762
|
-
"";
|
|
763
|
-
if (!fallback) return [];
|
|
764
|
-
return normalizeModelPatterns(fallback);
|
|
745
|
+
return resolveAgentModelPatterns({
|
|
746
|
+
agentModel: agent.model,
|
|
747
|
+
settings: this.#settingsManager ?? undefined,
|
|
748
|
+
activeModelPattern: this.modelContext.activeModelPattern,
|
|
749
|
+
fallbackModelPattern: this.modelContext.defaultModelPattern,
|
|
750
|
+
});
|
|
765
751
|
}
|
|
766
752
|
|
|
767
753
|
#effectivePatternsFor(agent: DashboardAgent, draftOverride: string | undefined): string[] {
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
754
|
+
return resolveAgentModelPatterns({
|
|
755
|
+
settingsOverride: draftOverride,
|
|
756
|
+
agentModel: agent.model,
|
|
757
|
+
settings: this.#settingsManager ?? undefined,
|
|
758
|
+
activeModelPattern: this.modelContext.activeModelPattern,
|
|
759
|
+
fallbackModelPattern: this.modelContext.defaultModelPattern,
|
|
760
|
+
});
|
|
773
761
|
}
|
|
774
762
|
|
|
775
763
|
#resolvePatterns(patterns: string[]): ModelResolution | undefined {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { AssistantMessage, ImageContent } from "@oh-my-pi/pi-ai";
|
|
1
|
+
import type { AssistantMessage, ImageContent, Usage } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import { Container, Image, ImageProtocol, Markdown, Spacer, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
3
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import { formatNumber, logger } from "@oh-my-pi/pi-utils";
|
|
4
|
+
import { settings } from "../../config/settings";
|
|
4
5
|
import { hasPendingMermaid, prerenderMermaid } from "../../modes/theme/mermaid-cache";
|
|
5
6
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
6
7
|
|
|
@@ -12,6 +13,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
12
13
|
#lastMessage?: AssistantMessage;
|
|
13
14
|
#prerenderInFlight = false;
|
|
14
15
|
#toolImagesByCallId = new Map<string, ImageContent[]>();
|
|
16
|
+
#usageInfo?: Usage;
|
|
15
17
|
|
|
16
18
|
constructor(
|
|
17
19
|
message?: AssistantMessage,
|
|
@@ -52,6 +54,13 @@ export class AssistantMessageComponent extends Container {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
setUsageInfo(usage: Usage): void {
|
|
58
|
+
this.#usageInfo = usage;
|
|
59
|
+
if (this.#lastMessage) {
|
|
60
|
+
this.updateContent(this.#lastMessage);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
55
64
|
#renderToolImages(): void {
|
|
56
65
|
const images = Array.from(this.#toolImagesByCallId.values()).flat();
|
|
57
66
|
if (images.length === 0) return;
|
|
@@ -178,5 +187,19 @@ export class AssistantMessageComponent extends Container {
|
|
|
178
187
|
this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
179
188
|
}
|
|
180
189
|
}
|
|
190
|
+
|
|
191
|
+
// Token usage metadata
|
|
192
|
+
if (settings.get("display.showTokenUsage") && this.#usageInfo) {
|
|
193
|
+
const usage = this.#usageInfo;
|
|
194
|
+
const totalInput = usage.input + usage.cacheWrite;
|
|
195
|
+
const parts: string[] = [];
|
|
196
|
+
parts.push(`${theme.icon.input} ${formatNumber(totalInput)}`);
|
|
197
|
+
parts.push(`${theme.icon.output} ${formatNumber(usage.output)}`);
|
|
198
|
+
if (usage.cacheRead > 0) {
|
|
199
|
+
parts.push(`cache: ${formatNumber(usage.cacheRead)}`);
|
|
200
|
+
}
|
|
201
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
202
|
+
this.#contentContainer.addChild(new Text(theme.fg("dim", parts.join(" ")), 1, 0));
|
|
203
|
+
}
|
|
181
204
|
}
|
|
182
205
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { type Component, Container, Markdown, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
2
|
+
import { replaceTabs } from "../../tools/render-utils";
|
|
3
|
+
import { getMarkdownTheme, theme } from "../theme/theme";
|
|
4
|
+
import { DynamicBorder } from "./dynamic-border";
|
|
5
|
+
|
|
6
|
+
type BtwPanelState = "running" | "complete" | "aborted" | "error";
|
|
7
|
+
|
|
8
|
+
interface BtwPanelComponentOptions {
|
|
9
|
+
question: string;
|
|
10
|
+
tui: TUI;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class BtwPanelComponent extends Container {
|
|
14
|
+
#question: string;
|
|
15
|
+
#tui: TUI;
|
|
16
|
+
#state: BtwPanelState = "running";
|
|
17
|
+
#answer = "";
|
|
18
|
+
#errorMessage: string | undefined;
|
|
19
|
+
#closed = false;
|
|
20
|
+
|
|
21
|
+
constructor(options: BtwPanelComponentOptions) {
|
|
22
|
+
super();
|
|
23
|
+
this.#question = options.question;
|
|
24
|
+
this.#tui = options.tui;
|
|
25
|
+
this.#rebuild();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
appendText(delta: string): void {
|
|
29
|
+
if (!delta || this.#closed) return;
|
|
30
|
+
this.#answer += delta;
|
|
31
|
+
this.#rebuild();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setAnswer(text: string): void {
|
|
35
|
+
if (this.#closed) return;
|
|
36
|
+
this.#answer = text;
|
|
37
|
+
this.#rebuild();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
markComplete(): void {
|
|
41
|
+
if (this.#closed) return;
|
|
42
|
+
this.#state = "complete";
|
|
43
|
+
this.#errorMessage = undefined;
|
|
44
|
+
this.#rebuild();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
markAborted(): void {
|
|
48
|
+
if (this.#closed) return;
|
|
49
|
+
this.#state = "aborted";
|
|
50
|
+
this.#errorMessage = undefined;
|
|
51
|
+
this.#rebuild();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
markError(message: string): void {
|
|
55
|
+
if (this.#closed) return;
|
|
56
|
+
this.#state = "error";
|
|
57
|
+
this.#errorMessage = message;
|
|
58
|
+
this.#rebuild();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
close(): void {
|
|
62
|
+
this.#closed = true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#rebuild(): void {
|
|
66
|
+
this.clear();
|
|
67
|
+
this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
|
|
68
|
+
this.addChild(new Spacer(1));
|
|
69
|
+
this.addChild(new Text(theme.fg("accent", replaceTabs(this.#question)), 1, 0));
|
|
70
|
+
this.addChild(new Spacer(1));
|
|
71
|
+
this.addChild(this.#contentComponent());
|
|
72
|
+
this.addChild(new Spacer(1));
|
|
73
|
+
this.addChild(new Text(this.#footerLine(), 1, 0));
|
|
74
|
+
this.addChild(new Spacer(1));
|
|
75
|
+
this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
|
|
76
|
+
this.#tui.requestRender();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
#footerLine(): string {
|
|
80
|
+
switch (this.#state) {
|
|
81
|
+
case "running":
|
|
82
|
+
return theme.fg("muted", "Esc cancel /btw");
|
|
83
|
+
case "complete":
|
|
84
|
+
return theme.fg("muted", "Esc dismiss");
|
|
85
|
+
case "aborted":
|
|
86
|
+
return theme.fg("warning", `${theme.status.warning} Cancelled · Esc dismiss`);
|
|
87
|
+
case "error":
|
|
88
|
+
return theme.fg("error", `${theme.status.error} Error · Esc dismiss`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#contentComponent(): Component {
|
|
93
|
+
if (this.#state === "error") {
|
|
94
|
+
return new Text(theme.fg("error", replaceTabs(this.#errorMessage ?? "Unknown error")), 1, 0);
|
|
95
|
+
}
|
|
96
|
+
const text = replaceTabs(this.#answer).trim();
|
|
97
|
+
if (!text) {
|
|
98
|
+
const waiting =
|
|
99
|
+
this.#state === "running" ? `${theme.status.pending} Waiting for response…` : "No text returned.";
|
|
100
|
+
return new Text(theme.fg("dim", waiting), 1, 0);
|
|
101
|
+
}
|
|
102
|
+
return new Markdown(text, 1, 0, getMarkdownTheme());
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -23,17 +23,12 @@ function visualizeIndent(text: string, filePath?: string): string {
|
|
|
23
23
|
const leftPadding = Math.floor(tabWidth / 2);
|
|
24
24
|
const rightPadding = Math.max(0, tabWidth - leftPadding - 1);
|
|
25
25
|
const tabMarker = `${DIM}${" ".repeat(leftPadding)}→${" ".repeat(rightPadding)}${DIM_OFF}`;
|
|
26
|
-
// Normalize: collapse configured tab-width groups into tab markers, then handle remaining spaces.
|
|
27
|
-
const normalized = indent.replaceAll("\t", indentation);
|
|
28
26
|
let visible = "";
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (pos + tabWidth <= normalized.length && normalized.slice(pos, pos + tabWidth) === indentation) {
|
|
27
|
+
for (const ch of indent) {
|
|
28
|
+
if (ch === "\t") {
|
|
32
29
|
visible += tabMarker;
|
|
33
|
-
pos += tabWidth;
|
|
34
30
|
} else {
|
|
35
31
|
visible += `${DIM}·${DIM_OFF}`;
|
|
36
|
-
pos++;
|
|
37
32
|
}
|
|
38
33
|
}
|
|
39
34
|
return `${visible}${replaceTabs(rest, filePath)}`;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* State manager for the Extension Control Center.
|
|
3
3
|
* Handles data loading, tree building, filtering, and toggle persistence.
|
|
4
4
|
*/
|
|
5
|
+
import * as path from "node:path";
|
|
5
6
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
6
7
|
import type { ContextFile } from "../../../capability/context-file";
|
|
7
8
|
import type { ExtensionModule } from "../../../capability/extension-module";
|
|
@@ -96,7 +97,7 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P
|
|
|
96
97
|
}
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
const loadOpts = cwd ? { cwd } : {};
|
|
100
|
+
const loadOpts = cwd ? { cwd, includeDisabled: true } : { includeDisabled: true };
|
|
100
101
|
|
|
101
102
|
// Load skills
|
|
102
103
|
try {
|
|
@@ -252,7 +253,7 @@ export async function loadAllExtensions(cwd?: string, disabledIds?: string[]): P
|
|
|
252
253
|
const contextFiles = await loadCapability<ContextFile>("context-files", loadOpts);
|
|
253
254
|
for (const file of contextFiles.all) {
|
|
254
255
|
// Extract filename from path for display
|
|
255
|
-
const name =
|
|
256
|
+
const name = path.basename(file.path);
|
|
256
257
|
const id = makeExtensionId("context-file", `${file.level}:${name}`);
|
|
257
258
|
const isDisabled = disabledExtensions.has(id);
|
|
258
259
|
const isShadowed = (file as { _shadowed?: boolean })._shadowed;
|
|
@@ -82,13 +82,29 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
82
82
|
// Context maintenance threshold
|
|
83
83
|
"compaction.thresholdPercent": [
|
|
84
84
|
{ value: "default", label: "Default", description: "Legacy reserve-based threshold" },
|
|
85
|
-
{ value: "
|
|
86
|
-
{ value: "
|
|
87
|
-
{ value: "
|
|
88
|
-
{ value: "
|
|
89
|
-
{ value: "
|
|
85
|
+
{ value: "10", label: "10%", description: "Extremely early maintenance" },
|
|
86
|
+
{ value: "20", label: "20%", description: "Very early maintenance" },
|
|
87
|
+
{ value: "30", label: "30%", description: "Early maintenance" },
|
|
88
|
+
{ value: "40", label: "40%", description: "Moderately early maintenance" },
|
|
89
|
+
{ value: "50", label: "50%", description: "Halfway point" },
|
|
90
|
+
{ value: "60", label: "60%", description: "Moderate context usage" },
|
|
91
|
+
{ value: "70", label: "70%", description: "Balanced" },
|
|
92
|
+
{ value: "75", label: "75%", description: "Slightly aggressive" },
|
|
93
|
+
{ value: "80", label: "80%", description: "Typical threshold" },
|
|
94
|
+
{ value: "85", label: "85%", description: "Aggressive context usage" },
|
|
95
|
+
{ value: "90", label: "90%", description: "Very aggressive" },
|
|
90
96
|
{ value: "95", label: "95%", description: "Near context limit" },
|
|
91
97
|
],
|
|
98
|
+
"compaction.thresholdTokens": [
|
|
99
|
+
{ value: "default", label: "Default", description: "Use percentage-based threshold" },
|
|
100
|
+
{ value: "25000", label: "25K tokens", description: "Quarter of a 200K window" },
|
|
101
|
+
{ value: "50000", label: "50K tokens", description: "Half of a 200K window" },
|
|
102
|
+
{ value: "100000", label: "100K tokens", description: "Half of a 200K window" },
|
|
103
|
+
{ value: "150000", label: "150K tokens", description: "Three-quarters of a 200K window" },
|
|
104
|
+
{ value: "200000", label: "200K tokens", description: "Full standard context window" },
|
|
105
|
+
{ value: "300000", label: "300K tokens", description: "Large context window" },
|
|
106
|
+
{ value: "500000", label: "500K tokens", description: "Very large context window" },
|
|
107
|
+
],
|
|
92
108
|
// Retry max retries
|
|
93
109
|
"retry.maxRetries": [
|
|
94
110
|
{ value: "1", label: "1 retry" },
|
|
@@ -190,6 +206,40 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
|
|
|
190
206
|
{ value: "300", label: "5 minutes" },
|
|
191
207
|
{ value: "600", label: "10 minutes" },
|
|
192
208
|
],
|
|
209
|
+
// Artifact spill settings
|
|
210
|
+
"tools.artifactSpillThreshold": [
|
|
211
|
+
{ value: "1", label: "1 KB", description: "~250 tokens" },
|
|
212
|
+
{ value: "2.5", label: "2.5 KB", description: "~625 tokens" },
|
|
213
|
+
{ value: "5", label: "5 KB", description: "~1.25K tokens" },
|
|
214
|
+
{ value: "10", label: "10 KB", description: "~2.5K tokens" },
|
|
215
|
+
{ value: "20", label: "20 KB", description: "~5K tokens" },
|
|
216
|
+
{ value: "30", label: "30 KB", description: "~7.5K tokens" },
|
|
217
|
+
{ value: "50", label: "50 KB", description: "Default; ~12.5K tokens" },
|
|
218
|
+
{ value: "75", label: "75 KB", description: "~19K tokens" },
|
|
219
|
+
{ value: "100", label: "100 KB", description: "~25K tokens" },
|
|
220
|
+
{ value: "200", label: "200 KB", description: "~50K tokens" },
|
|
221
|
+
{ value: "500", label: "500 KB", description: "~125K tokens" },
|
|
222
|
+
{ value: "1000", label: "1 MB", description: "~250K tokens" },
|
|
223
|
+
],
|
|
224
|
+
"tools.artifactTailBytes": [
|
|
225
|
+
{ value: "1", label: "1 KB", description: "~250 tokens" },
|
|
226
|
+
{ value: "2.5", label: "2.5 KB", description: "~625 tokens" },
|
|
227
|
+
{ value: "5", label: "5 KB", description: "~1.25K tokens" },
|
|
228
|
+
{ value: "10", label: "10 KB", description: "~2.5K tokens" },
|
|
229
|
+
{ value: "20", label: "20 KB", description: "Default; ~5K tokens" },
|
|
230
|
+
{ value: "50", label: "50 KB", description: "~12.5K tokens" },
|
|
231
|
+
{ value: "100", label: "100 KB", description: "~25K tokens" },
|
|
232
|
+
{ value: "200", label: "200 KB", description: "~50K tokens" },
|
|
233
|
+
],
|
|
234
|
+
"tools.artifactTailLines": [
|
|
235
|
+
{ value: "50", label: "50 lines", description: "~250 tokens" },
|
|
236
|
+
{ value: "100", label: "100 lines", description: "~500 tokens" },
|
|
237
|
+
{ value: "250", label: "250 lines", description: "~1.25K tokens" },
|
|
238
|
+
{ value: "500", label: "500 lines", description: "Default; ~2.5K tokens" },
|
|
239
|
+
{ value: "1000", label: "1000 lines", description: "~5K tokens" },
|
|
240
|
+
{ value: "2000", label: "2000 lines", description: "~10K tokens" },
|
|
241
|
+
{ value: "5000", label: "5000 lines", description: "~25K tokens" },
|
|
242
|
+
],
|
|
193
243
|
// Read line limit
|
|
194
244
|
"read.defaultLimit": [
|
|
195
245
|
{ value: "200", label: "200 lines" },
|
|
@@ -419,7 +469,7 @@ export function getAllSettingDefs(): SettingDef[] {
|
|
|
419
469
|
if (cachedDefs) return cachedDefs;
|
|
420
470
|
|
|
421
471
|
const defs: SettingDef[] = [];
|
|
422
|
-
for (const tab of
|
|
472
|
+
for (const tab of SETTING_TABS) {
|
|
423
473
|
for (const path of getPathsForTab(tab)) {
|
|
424
474
|
const def = pathToSettingDef(path);
|
|
425
475
|
if (def) defs.push(def);
|