pi-soly 1.11.2 → 1.12.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 +60 -14
- package/artifact/index.ts +241 -0
- package/artifact/render.ts +239 -0
- package/artifact/server.ts +384 -0
- package/artifact/session.ts +368 -0
- package/ask/README.md +20 -8
- package/ask/index.ts +100 -36
- package/ask/picker.ts +345 -29
- package/commands.ts +246 -30
- package/config.ts +124 -8
- package/core.ts +48 -236
- package/deck/deck.ts +386 -0
- package/deck/index.ts +113 -0
- package/index.ts +153 -37
- package/iteration.ts +1 -9
- package/mcp/commands.ts +12 -0
- package/mcp/direct-tools.ts +19 -2
- package/mcp/index.ts +144 -2
- package/mcp/init.ts +11 -0
- package/mcp/lifecycle.ts +9 -2
- package/mcp/server-manager.ts +30 -18
- package/mcp/state.ts +7 -0
- package/mcp/tool-cache.ts +135 -0
- package/nudge.ts +20 -3
- package/package.json +10 -3
- package/skills/soly-framework/SKILL.md +64 -37
- package/tool-hints.ts +62 -0
- package/tools.ts +28 -100
- package/util.ts +250 -0
- package/visual/chrome.ts +166 -0
- package/visual/colors.ts +24 -0
- package/visual/data.ts +62 -0
- package/visual/footer.ts +129 -0
- package/visual/format.ts +73 -0
- package/visual/glyphs.ts +35 -0
- package/visual/gradient.ts +122 -0
- package/visual/index.ts +18 -0
- package/visual/list-panel.ts +207 -0
- package/visual/segments.ts +136 -0
- package/visual/style.ts +34 -0
- package/visual/topbar.ts +55 -0
- package/visual/welcome.ts +265 -0
- package/visual/working.ts +66 -0
- package/workflows/execute.ts +17 -7
- package/workflows/index.ts +91 -44
- package/workflows/inspect.ts +23 -26
- package/workflows/migrate.ts +85 -0
- package/workflows/parser.ts +4 -2
- package/workflows/planning.ts +9 -8
- package/workflows/verify.ts +194 -0
- package/workflows-data/discuss-phase.md +10 -6
- package/agents-install.ts +0 -106
- package/ask/prompt.ts +0 -41
- package/ask/tests/prompt.test.ts +0 -54
package/deck/index.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// index.ts — pi-deck extension entry point
|
|
3
|
+
// =============================================================================
|
|
4
|
+
//
|
|
5
|
+
// Registers one LLM tool: `decision_deck`. Presents a single design/architecture
|
|
6
|
+
// decision as a full-screen stack of cards (one per option) that the user flips
|
|
7
|
+
// through and picks from. Each card carries a title, summary, optional code
|
|
8
|
+
// snippet, and pros/cons — so the user compares concrete shapes, not just labels.
|
|
9
|
+
//
|
|
10
|
+
// When to reach for it over ask_pro: the choice hinges on seeing the code/structure
|
|
11
|
+
// of each option side-by-side, and a thin preview column isn't enough. Native TUI
|
|
12
|
+
// (no browser, no server). Returns the chosen option index in one call.
|
|
13
|
+
// =============================================================================
|
|
14
|
+
|
|
15
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { highlightCode } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { Type } from "typebox";
|
|
18
|
+
import { DeckComponent, type DeckResult } from "./deck.ts";
|
|
19
|
+
|
|
20
|
+
export default function piDeckExtension(pi: ExtensionAPI) {
|
|
21
|
+
// Usage guidance lives in the soly-framework skill + the main soly prompt
|
|
22
|
+
// pointer — not injected here.
|
|
23
|
+
pi.registerTool({
|
|
24
|
+
name: "decision_deck",
|
|
25
|
+
label: "soly · decision_deck",
|
|
26
|
+
description:
|
|
27
|
+
"Present ONE design/architecture decision as a full-screen deck of cards (one per option) the user flips through (←/→ or 1-N) and picks with Enter. Use instead of ask_pro when the choice hinges on comparing each option's concrete code shape, not a label. 2-6 options. The user can attach a free-text note (rationale, caveats) via `n`; it's returned in the result. Native TUI. Returns the chosen option.",
|
|
28
|
+
parameters: Type.Object({
|
|
29
|
+
title: Type.Optional(Type.String({ description: "Decision title." })),
|
|
30
|
+
prompt: Type.Optional(Type.String({ description: "Question shown above the cards." })),
|
|
31
|
+
options: Type.Array(
|
|
32
|
+
Type.Object({
|
|
33
|
+
title: Type.String({ description: "Card title." }),
|
|
34
|
+
summary: Type.Optional(Type.String({ description: "1-3 sentence explanation." })),
|
|
35
|
+
code: Type.Optional(
|
|
36
|
+
Type.String({ description: "Raw code snippet (no fences); set `lang` to highlight." }),
|
|
37
|
+
),
|
|
38
|
+
lang: Type.Optional(Type.String({ description: "Highlight language (e.g. 'ts')." })),
|
|
39
|
+
pros: Type.Optional(Type.Array(Type.String(), { description: "Upsides (+)." })),
|
|
40
|
+
cons: Type.Optional(Type.Array(Type.String(), { description: "Downsides (−)." })),
|
|
41
|
+
recommended: Type.Optional(
|
|
42
|
+
Type.Boolean({ description: "⭐ recommended (cursor starts here, at most one)." }),
|
|
43
|
+
),
|
|
44
|
+
}),
|
|
45
|
+
{ description: "2-6 options to compare." },
|
|
46
|
+
),
|
|
47
|
+
}),
|
|
48
|
+
async execute(_id, params, _signal, _onUpdate, ctx) {
|
|
49
|
+
if (!ctx.hasUI) {
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: "decision_deck requires a UI-capable session (TUI). Run from the interactive pi TUI.",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
details: { error: "no_ui", mode: ctx.mode },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (params.options.length < 2 || params.options.length > 6) {
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: `decision_deck: ${params.options.length} options, need 2-6.`,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
details: { error: "bad_option_count", count: params.options.length },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
const recommended = params.options.filter((o) => o.recommended).length;
|
|
72
|
+
if (recommended > 1) {
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{ type: "text", text: `decision_deck: ${recommended} recommended, at most 1 allowed.` },
|
|
76
|
+
],
|
|
77
|
+
details: { error: "multiple_recommended" },
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const result = await ctx.ui.custom<DeckResult>((_tui, theme, keybindings, done) => {
|
|
82
|
+
const deckTheme = theme as unknown as ConstructorParameters<typeof DeckComponent>[0]["theme"];
|
|
83
|
+
return new DeckComponent({
|
|
84
|
+
options: params.options,
|
|
85
|
+
theme: deckTheme,
|
|
86
|
+
keybindings,
|
|
87
|
+
done,
|
|
88
|
+
title: params.title ?? "decision",
|
|
89
|
+
prompt: params.prompt,
|
|
90
|
+
highlight: highlightCode,
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (result.cancelled || result.chosen === undefined) {
|
|
95
|
+
return {
|
|
96
|
+
content: [{ type: "text", text: "(user cancelled the deck — no choice made)" }],
|
|
97
|
+
details: { cancelled: true },
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const chosen = result.chosen;
|
|
102
|
+
const opt = params.options[chosen];
|
|
103
|
+
const note = result.note?.trim() ?? "";
|
|
104
|
+
const text = note
|
|
105
|
+
? `User chose option ${chosen + 1}: "${opt?.title ?? "?"}". // note: "${note}"`
|
|
106
|
+
: `User chose option ${chosen + 1}: "${opt?.title ?? "?"}".`;
|
|
107
|
+
return {
|
|
108
|
+
content: [{ type: "text", text }],
|
|
109
|
+
details: { chosen, title: opt?.title, note: note || undefined },
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
}
|
package/index.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import * as os from "node:os";
|
|
23
23
|
import * as path from "node:path";
|
|
24
24
|
import * as fs from "node:fs";
|
|
25
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
26
26
|
|
|
27
27
|
import {
|
|
28
28
|
analyzeRules,
|
|
@@ -48,7 +48,6 @@ import {
|
|
|
48
48
|
type SourceSpec,
|
|
49
49
|
} from "./core.ts";
|
|
50
50
|
import { buildIntegrationsSection } from "./integrations.ts";
|
|
51
|
-
import { installSolyAssets } from "./agents-install.ts";
|
|
52
51
|
import {
|
|
53
52
|
DEFAULT_CONFIG,
|
|
54
53
|
loadConfig,
|
|
@@ -56,6 +55,7 @@ import {
|
|
|
56
55
|
type SolyConfig,
|
|
57
56
|
} from "./config.ts";
|
|
58
57
|
import { classifyTaskHeuristics, buildNudgeSection } from "./nudge.ts";
|
|
58
|
+
import { detectToolHints, buildToolHintSection } from "./tool-hints.ts";
|
|
59
59
|
import { notifyNudge, notifyDeprecation } from "./notification.ts";
|
|
60
60
|
import { registerCommands, type CommandUI } from "./commands.ts";
|
|
61
61
|
import { registerTools } from "./tools.ts";
|
|
@@ -66,9 +66,35 @@ import { detectEnv, buildEnvSection, type EnvSummary } from "./env.ts";
|
|
|
66
66
|
import { buildCodeMap, buildCodeMapSection, type CodeMap } from "./codemap.ts";
|
|
67
67
|
import { loadIntentDocs, buildIntentSection, loadInlineIntentBodies, type IntentDoc } from "./intent.ts";
|
|
68
68
|
|
|
69
|
+
import { createChrome, readWelcomeMeta } from "./visual/index.ts";
|
|
70
|
+
import { createContextManager } from "./context-manager.ts";
|
|
71
|
+
|
|
69
72
|
// Built-in sub-features (merged from former pi-asked, pi-agented packages):
|
|
70
73
|
import piAskExtension from "./ask/index.ts";
|
|
74
|
+
import piDeckExtension from "./deck/index.ts";
|
|
75
|
+
import piArtifactExtension from "./artifact/index.ts";
|
|
76
|
+
import { getArtifactServer } from "./artifact/session.ts";
|
|
77
|
+
|
|
78
|
+
/** Compact phase label for the chrome top bar, e.g. "plan 2/5". Null when idle. */
|
|
79
|
+
function phaseLabelFromState(s: SolyState): string | null {
|
|
80
|
+
if (!s.exists) return null;
|
|
81
|
+
const cur = s.currentPhase;
|
|
82
|
+
const total = s.phases.length;
|
|
83
|
+
if (cur) return total > 0 ? `${cur.slug || cur.name} ${cur.number}/${total}` : `${cur.slug || cur.name}`;
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
|
|
88
|
+
/** Tiny always-on pointer to soly's interactive/visual tools. The detailed
|
|
89
|
+
* "when / when-not / params" guidance lives in the soly-framework skill
|
|
90
|
+
* (loaded on demand) so it doesn't cost tokens every turn. */
|
|
91
|
+
const SOLY_TOOLS_POINTER = `
|
|
92
|
+
## soly — interactive & visual tools
|
|
93
|
+
|
|
94
|
+
When terminal text isn't the best medium, reach for these (details + when-NOT in the soly-framework skill):
|
|
95
|
+
- \`ask_pro\` — multi-question picker (single/multi-select, free-text, per-option previews).
|
|
96
|
+
- \`decision_deck\` — full-screen cards comparing design options by their concrete code shape.
|
|
97
|
+
- \`html_artifact\` — render HTML to a self-contained, browseable per-project gallery.`;
|
|
72
98
|
|
|
73
99
|
export default function solyExtension(pi: ExtensionAPI) {
|
|
74
100
|
// ============================================================================
|
|
@@ -154,6 +180,13 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
154
180
|
let intentDocs: IntentDoc[] = [];
|
|
155
181
|
let lastIntentSection = "";
|
|
156
182
|
|
|
183
|
+
// Visual chrome (top bar + custom footer + working spinner). Config-gated.
|
|
184
|
+
const chrome = createChrome(() => getActiveConfig().chrome);
|
|
185
|
+
|
|
186
|
+
// Single owner of pi's `context` channel (used by the /verify fresh-context
|
|
187
|
+
// mode; future context strategies plug in here too).
|
|
188
|
+
const contextManager = createContextManager(pi);
|
|
189
|
+
|
|
157
190
|
// ============================================================================
|
|
158
191
|
// Loaders
|
|
159
192
|
// ============================================================================
|
|
@@ -297,6 +330,35 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
297
330
|
}
|
|
298
331
|
};
|
|
299
332
|
|
|
333
|
+
// Refresh the chrome's live data snapshot (cwd, model, ctx%, git, phase)
|
|
334
|
+
// from the current context + project state, then request a re-render.
|
|
335
|
+
const updateChromeData = (ctx: ExtensionContext) => {
|
|
336
|
+
if (!getActiveConfig().chrome.enabled) return;
|
|
337
|
+
const d = chrome.data;
|
|
338
|
+
d.cwd = sessionCwd || ctx.cwd || "";
|
|
339
|
+
d.home = os.homedir();
|
|
340
|
+
const model = ctx.model as { id?: string; provider?: string; reasoning?: boolean } | undefined;
|
|
341
|
+
d.modelId = model?.id ?? null;
|
|
342
|
+
d.modelProvider = model?.provider ?? null;
|
|
343
|
+
d.reasoning = Boolean(model?.reasoning);
|
|
344
|
+
try {
|
|
345
|
+
d.thinkingLevel = pi.getThinkingLevel();
|
|
346
|
+
} catch {
|
|
347
|
+
d.thinkingLevel = null;
|
|
348
|
+
}
|
|
349
|
+
const usage = ctx.getContextUsage();
|
|
350
|
+
d.ctxPercent = usage?.percent ?? null;
|
|
351
|
+
d.ctxTokens = usage?.tokens ?? null;
|
|
352
|
+
d.contextWindow = usage?.contextWindow ?? null;
|
|
353
|
+
d.gitDirty = gitContext.statusShort
|
|
354
|
+
? gitContext.statusShort.split(/\r?\n/).filter(Boolean).length
|
|
355
|
+
: 0;
|
|
356
|
+
d.rulesActive = combinedRules().length;
|
|
357
|
+
d.phaseLabel = phaseLabelFromState(state);
|
|
358
|
+
d.artifactCount = getArtifactServer()?.count ?? 0;
|
|
359
|
+
chrome.poke();
|
|
360
|
+
};
|
|
361
|
+
|
|
300
362
|
// ============================================================================
|
|
301
363
|
// Register sub-features
|
|
302
364
|
// ============================================================================
|
|
@@ -327,6 +389,15 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
327
389
|
getActiveTools: () => pi.getActiveTools(),
|
|
328
390
|
getConfig: getActiveConfig,
|
|
329
391
|
onWorkflowUsed: resetSolyDrift,
|
|
392
|
+
contextManager,
|
|
393
|
+
onVerifyState: (s) => {
|
|
394
|
+
chrome.data.verbLabel = s.active ? (s.iteration > 0 ? `verify ${s.iteration}/${s.max}` : "verify") : null;
|
|
395
|
+
chrome.poke();
|
|
396
|
+
},
|
|
397
|
+
setVerbLabel: (verb) => {
|
|
398
|
+
chrome.data.verbLabel = verb;
|
|
399
|
+
chrome.poke();
|
|
400
|
+
},
|
|
330
401
|
});
|
|
331
402
|
|
|
332
403
|
// ============================================================================
|
|
@@ -336,7 +407,7 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
336
407
|
pi.on("session_start", async (event, ctx) => {
|
|
337
408
|
// Deprecation warning: if the project still uses `.soly/`, nudge the
|
|
338
409
|
// user toward `.agents/`. One-time per session.
|
|
339
|
-
if (
|
|
410
|
+
if (isLegacySolyDir(ctx.cwd)) {
|
|
340
411
|
notifyDeprecation(
|
|
341
412
|
ctx.ui,
|
|
342
413
|
`.soly/ (legacy)`,
|
|
@@ -387,42 +458,13 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
387
458
|
}
|
|
388
459
|
}
|
|
389
460
|
|
|
390
|
-
//
|
|
391
|
-
//
|
|
392
|
-
// first run. Opt-in via config `agent.useSolyWorkerSubagents`
|
|
393
|
-
// (kept for backward compat, now a no-op for the skill install).
|
|
394
|
-
// Idempotent — respects any existing user-customized copies.
|
|
395
|
-
if (activeConfig.agent.useSolyWorkerSubagents) {
|
|
396
|
-
const extRoot = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
|
|
397
|
-
const assets = installSolyAssets(extRoot);
|
|
398
|
-
const installed = assets.skills.installed.map((s) => `skill:${s}`);
|
|
399
|
-
if (installed.length > 0) {
|
|
400
|
-
ctx.ui.notify(
|
|
401
|
-
`soly: installed (${installed.join(", ")}) — pi will discover on next session`,
|
|
402
|
-
"info",
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
for (const e of assets.skills.errors) {
|
|
406
|
-
ctx.ui.notify(`soly: install error — ${e}`, "warning");
|
|
407
|
-
}
|
|
408
|
-
}
|
|
461
|
+
// (The soly-framework skill now ships via the package `pi.skills`
|
|
462
|
+
// manifest — pi discovers it natively, no home-dir copy needed.)
|
|
409
463
|
|
|
410
464
|
// Phase-scoped rules: if a phase is currently active, load its
|
|
411
465
|
// per-phase rules on top of the always-on rule set.
|
|
412
466
|
reloadPhaseRules();
|
|
413
467
|
|
|
414
|
-
// Restore agent from .soly/agent (if present) — survives session restart
|
|
415
|
-
if (state.exists) {
|
|
416
|
-
const agentFile = path.join(state.solyDir, "agent");
|
|
417
|
-
try {
|
|
418
|
-
if (fs.existsSync(agentFile)) {
|
|
419
|
-
const raw = fs.readFileSync(agentFile, "utf-8").trim();
|
|
420
|
-
if (raw && /^[a-zA-Z0-9_-]{1,64}$/.test(raw)) {
|
|
421
|
-
(globalThis as { __PI_SWITCH_AGENT__?: string }).__PI_SWITCH_AGENT__ = raw;
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
} catch { /* best-effort */ }
|
|
425
|
-
}
|
|
426
468
|
|
|
427
469
|
// Capture rule mtimes for the "rules changed since last session" diff
|
|
428
470
|
captureLastSessionRuleMtimes();
|
|
@@ -534,19 +576,45 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
534
576
|
}
|
|
535
577
|
|
|
536
578
|
updateStatus(ctx);
|
|
579
|
+
|
|
580
|
+
// Install the visual chrome (top bar + custom footer + snowflake spinner).
|
|
581
|
+
chrome.install(ctx.ui);
|
|
582
|
+
updateChromeData(ctx);
|
|
583
|
+
|
|
584
|
+
// Startup header snapshot (version + recent changes + project state).
|
|
585
|
+
try {
|
|
586
|
+
const extRoot = path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1"));
|
|
587
|
+
const meta = readWelcomeMeta(extRoot);
|
|
588
|
+
chrome.setWelcome({
|
|
589
|
+
version: meta.version,
|
|
590
|
+
hasProject: state.exists,
|
|
591
|
+
phaseLabel: phaseLabelFromState(state),
|
|
592
|
+
nextHint: buildNextHint(state) || null,
|
|
593
|
+
rulesActive: combinedRules().length,
|
|
594
|
+
docsCount: intentDocs.length,
|
|
595
|
+
recent: meta.recent,
|
|
596
|
+
});
|
|
597
|
+
} catch {
|
|
598
|
+
/* header is best-effort; never block session start */
|
|
599
|
+
}
|
|
537
600
|
});
|
|
538
601
|
|
|
539
|
-
pi.on("session_shutdown", async (_event,
|
|
602
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
540
603
|
// Stop hot-reload watcher — fs.watch handles hold OS resources
|
|
541
604
|
if (hotReload) {
|
|
542
605
|
hotReload.stop();
|
|
543
606
|
hotReload = null;
|
|
544
607
|
}
|
|
608
|
+
// Restore pi's native footer/widgets/indicator before teardown
|
|
609
|
+
chrome.dispose(ctx.ui);
|
|
545
610
|
// Persist rule mtimes so the next session can show the diff
|
|
546
611
|
persistRuleMtimes();
|
|
547
612
|
});
|
|
548
613
|
|
|
549
614
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
615
|
+
// Keep the chrome (ctx%, model, phase) current for the upcoming turn.
|
|
616
|
+
updateChromeData(ctx);
|
|
617
|
+
|
|
550
618
|
const sections: string[] = [];
|
|
551
619
|
let totalRulesTokens = 0;
|
|
552
620
|
|
|
@@ -590,6 +658,9 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
590
658
|
sections.push(integrationSection);
|
|
591
659
|
}
|
|
592
660
|
|
|
661
|
+
// 2.6. Interactive/visual tools pointer (lean — detail lives in the skill).
|
|
662
|
+
sections.push(SOLY_TOOLS_POINTER);
|
|
663
|
+
|
|
593
664
|
// 3.5. Project intent (zero-point docs) — always injected when present
|
|
594
665
|
if (lastIntentSection) {
|
|
595
666
|
sections.push(lastIntentSection);
|
|
@@ -618,7 +689,19 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
618
689
|
|
|
619
690
|
// 7. Behavioral nudge
|
|
620
691
|
const heuristics = classifyTaskHeuristics(event.prompt);
|
|
621
|
-
sections.push(
|
|
692
|
+
sections.push(
|
|
693
|
+
buildNudgeSection(heuristics, {
|
|
694
|
+
hasProject: state.exists,
|
|
695
|
+
confirmBeforeCode: getActiveConfig().agent.confirmBeforeCode,
|
|
696
|
+
}),
|
|
697
|
+
);
|
|
698
|
+
|
|
699
|
+
// 7.1 Interactive-tool affordance hints (examples → html_artifact, options
|
|
700
|
+
// → decision_deck, …) — only on turns whose wording mentions them.
|
|
701
|
+
if (getActiveConfig().agent.toolHints) {
|
|
702
|
+
const toolHint = buildToolHintSection(detectToolHints(event.prompt));
|
|
703
|
+
if (toolHint) sections.push(toolHint);
|
|
704
|
+
}
|
|
622
705
|
|
|
623
706
|
// 7.5 Soly drift reminder — injected when the user has been doing
|
|
624
707
|
// non-soly work for several turns. Throttled: at most once per
|
|
@@ -662,7 +745,11 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
662
745
|
const angle =
|
|
663
746
|
heuristics.suggestedAngles[0] ?? "want me to confirm assumptions before I start?";
|
|
664
747
|
|
|
665
|
-
|
|
748
|
+
// The visible chat box is opt-in (default off — it was noisy). The
|
|
749
|
+
// system-prompt nudge still guides the LLM on every turn regardless.
|
|
750
|
+
if (getActiveConfig().agent.nudgeNotify) {
|
|
751
|
+
notifyNudge(ctx.ui, heuristics.researchHeavy ? "research" : "nonTrivial", angle);
|
|
752
|
+
}
|
|
666
753
|
nudgeActiveForTask = true;
|
|
667
754
|
lastNudgePromptKey = text.slice(0, 200);
|
|
668
755
|
|
|
@@ -719,6 +806,9 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
719
806
|
updateStatus(ctx);
|
|
720
807
|
}
|
|
721
808
|
|
|
809
|
+
// Chrome reflects fresh ctx%, session tokens, git dirty count, phase.
|
|
810
|
+
updateChromeData(ctx);
|
|
811
|
+
|
|
722
812
|
// Post-work rules check: surface applicable rules for files edited
|
|
723
813
|
// in this turn. Tracking is kept silent (no chat notify — user feedback:
|
|
724
814
|
// "spammy") but data is preserved for /why to show last-turn rule context.
|
|
@@ -744,8 +834,34 @@ export default function solyExtension(pi: ExtensionAPI) {
|
|
|
744
834
|
}
|
|
745
835
|
});
|
|
746
836
|
|
|
837
|
+
// ============================================================================
|
|
838
|
+
// Chrome: working-indicator telemetry lifecycle + reactive data refresh
|
|
839
|
+
// ============================================================================
|
|
840
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
841
|
+
updateChromeData(ctx); // snapshot ctx tokens (↑) before generation starts
|
|
842
|
+
chrome.startWorking(ctx.ui);
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
pi.on("message_update", async (event, ctx) => {
|
|
846
|
+
const msg = event.message as { role?: string; usage?: { output?: number } };
|
|
847
|
+
if (msg.role === "assistant" && typeof msg.usage?.output === "number") {
|
|
848
|
+
chrome.updateWorking(ctx.ui, msg.usage.output);
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
853
|
+
chrome.stopWorking(ctx.ui);
|
|
854
|
+
updateChromeData(ctx);
|
|
855
|
+
});
|
|
856
|
+
|
|
857
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
858
|
+
updateChromeData(ctx);
|
|
859
|
+
});
|
|
860
|
+
|
|
747
861
|
// Mount built-in sub-features
|
|
748
862
|
piAskExtension(pi);
|
|
863
|
+
piDeckExtension(pi);
|
|
864
|
+
piArtifactExtension(pi, getActiveConfig);
|
|
749
865
|
|
|
750
866
|
// MCP adapter (was: separate pi-mcp-adapter plugin by nicobailon).
|
|
751
867
|
// Bundled into pi-soly as of v1.11.0 with UE5 session-retry fix + framed
|
package/iteration.ts
CHANGED
|
@@ -579,16 +579,8 @@ export function buildIterationContent(input: IterationInput): string {
|
|
|
579
579
|
}
|
|
580
580
|
|
|
581
581
|
if ((input.kind === "plan" || input.kind === "exec") && phaseDir) {
|
|
582
|
-
// Section 3: Phase CONTEXT
|
|
583
|
-
const ctxPath = findPhaseContextPath(phaseDir);
|
|
584
|
-
sections.push("## 3. Phase CONTEXT");
|
|
585
|
-
sections.push("");
|
|
586
|
-
sections.push(`_Source: \`${ctxPath ? rel(projectRoot, ctxPath) : "(missing)"}\`_`);
|
|
587
|
-
sections.push("");
|
|
588
|
-
sections.push(loadPhaseContext(phaseDir, SECTION_BUDGETS.context));
|
|
589
|
-
sections.push("");
|
|
590
|
-
|
|
591
582
|
// Section 4: Phase RESEARCH
|
|
583
|
+
// (Section 3 Phase CONTEXT is emitted by the plan|exec|discuss block above.)
|
|
592
584
|
const resPath = findPhaseResearchPath(phaseDir);
|
|
593
585
|
sections.push("## 4. Phase RESEARCH");
|
|
594
586
|
sections.push("");
|
package/mcp/commands.ts
CHANGED
|
@@ -58,6 +58,18 @@ export async function showStatus(state: McpExtensionState, ctx: ExtensionContext
|
|
|
58
58
|
lines.push("Run /mcp setup to adopt imports or scaffold a starter .mcp.json");
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// Tool result cache stats — appended after the per-server list. Hidden if
|
|
62
|
+
// the cache has not seen any traffic yet (fresh session, no calls made).
|
|
63
|
+
const cacheStats = state.toolCache?.stats();
|
|
64
|
+
if (cacheStats && (cacheStats.hits > 0 || cacheStats.misses > 0 || cacheStats.expirations > 0 || cacheStats.size > 0)) {
|
|
65
|
+
const total = cacheStats.hits + cacheStats.misses;
|
|
66
|
+
const hitRate = total > 0 ? Math.round((cacheStats.hits / total) * 100) : 0;
|
|
67
|
+
lines.push("");
|
|
68
|
+
lines.push(
|
|
69
|
+
`Cache: ${cacheStats.size} entries · ${cacheStats.hits} hits · ${cacheStats.misses} misses · ${cacheStats.expirations} expired (${hitRate}% hit rate)`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
61
73
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
62
74
|
}
|
|
63
75
|
|
package/mcp/direct-tools.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { formatToolName, isToolExcluded } from "./types.ts";
|
|
|
12
12
|
import { resourceNameToToolName } from "./resource-tools.ts";
|
|
13
13
|
import { authenticate, supportsOAuth } from "./mcp-auth-flow.ts";
|
|
14
14
|
import { formatAuthRequiredMessage } from "./utils.ts";
|
|
15
|
+
import { ToolCache, cacheKey } from "./tool-cache.ts";
|
|
15
16
|
|
|
16
17
|
const BUILTIN_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
17
18
|
|
|
@@ -274,9 +275,19 @@ type DirectToolExecute = (
|
|
|
274
275
|
export function createDirectToolExecutor(
|
|
275
276
|
getState: () => McpExtensionState | null,
|
|
276
277
|
getInitPromise: () => Promise<McpExtensionState> | null,
|
|
277
|
-
spec: DirectToolSpec
|
|
278
|
+
spec: DirectToolSpec,
|
|
279
|
+
cache?: ToolCache,
|
|
278
280
|
): DirectToolExecute {
|
|
279
281
|
return async function execute(_toolCallId, params) {
|
|
282
|
+
// Cache fast-path: skip connect/init/auth on hit. UI tools and resources
|
|
283
|
+
// are never cached (UI has side effects, user asked for tools only).
|
|
284
|
+
if (cache && !spec.resourceUri && !spec.uiResourceUri) {
|
|
285
|
+
const hit = cache.get(cacheKey(spec.serverName, spec.originalName, params));
|
|
286
|
+
if (hit !== undefined) {
|
|
287
|
+
return hit as Awaited<ReturnType<DirectToolExecute>>;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
280
291
|
let state = getState();
|
|
281
292
|
const initPromise = getInitPromise();
|
|
282
293
|
|
|
@@ -402,10 +413,16 @@ export function createDirectToolExecutor(
|
|
|
402
413
|
};
|
|
403
414
|
}
|
|
404
415
|
|
|
405
|
-
|
|
416
|
+
const successResult: Awaited<ReturnType<DirectToolExecute>> = {
|
|
406
417
|
content: content.length > 0 ? content : [{ type: "text" as const, text: "(empty result)" }],
|
|
407
418
|
details: { server: spec.serverName, tool: spec.originalName },
|
|
408
419
|
};
|
|
420
|
+
// Cache only plain (non-UI) tool success; resources and UI tools are
|
|
421
|
+
// never cached (see fast-path above for the matching skip condition).
|
|
422
|
+
if (cache) {
|
|
423
|
+
cache.set(cacheKey(spec.serverName, spec.originalName, params), successResult);
|
|
424
|
+
}
|
|
425
|
+
return successResult;
|
|
409
426
|
} catch (error) {
|
|
410
427
|
if (error instanceof UrlElicitationRequiredError) {
|
|
411
428
|
const action = await state.manager.handleUrlElicitationRequired(spec.serverName, error);
|