pi-soly 1.11.2 → 1.12.1
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/commands.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
// subcommands: position, state, plan, context, research, roadmap,
|
|
10
10
|
// progress, phases, tasks, task <id>, features,
|
|
11
11
|
// milestone, reload, help
|
|
12
|
+
// - /artifacts browse this session's html_artifact gallery (list/open/clear)
|
|
12
13
|
// - /rulewizard interactive guide for rule vs .editorconfig vs linter
|
|
13
14
|
// - /why show rules + project state that grounded the last turn
|
|
14
15
|
//
|
|
@@ -18,7 +19,8 @@
|
|
|
18
19
|
|
|
19
20
|
import * as path from "node:path";
|
|
20
21
|
import * as fs from "node:fs";
|
|
21
|
-
import
|
|
22
|
+
import { platform } from "node:os";
|
|
23
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
22
24
|
import {
|
|
23
25
|
analyzeRules,
|
|
24
26
|
buildProgressBar,
|
|
@@ -43,8 +45,8 @@ import {
|
|
|
43
45
|
import type { SolyConfig } from "./config.ts";
|
|
44
46
|
import { migrateSolyDir } from "./migrate.js";
|
|
45
47
|
import { initSolyProject } from "./init.js";
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
+
import { ListPanel, type ListItem, type ListAction } from "./visual/list-panel.ts";
|
|
49
|
+
import { getArtifactServer, ensureArtifactServer, artifactDir } from "./artifact/session.ts";
|
|
48
50
|
|
|
49
51
|
/** Minimum ui surface the command handlers actually need. */
|
|
50
52
|
export interface CommandUI {
|
|
@@ -65,6 +67,68 @@ export interface CommandsDeps {
|
|
|
65
67
|
getIntentDocs: () => IntentDoc[];
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
/** Open a focused list modal (overlay) for the given items + actions. */
|
|
71
|
+
async function openListPanel(
|
|
72
|
+
ctx: ExtensionCommandContext,
|
|
73
|
+
spec: {
|
|
74
|
+
title: string;
|
|
75
|
+
headerRight?: string;
|
|
76
|
+
build: () => ListItem[];
|
|
77
|
+
actions?: ListAction[];
|
|
78
|
+
onSelect?: (item: ListItem) => void;
|
|
79
|
+
},
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
await ctx.ui.custom<void>(
|
|
82
|
+
(tui, theme, keybindings, done) =>
|
|
83
|
+
new ListPanel({
|
|
84
|
+
tui,
|
|
85
|
+
theme,
|
|
86
|
+
keybindings,
|
|
87
|
+
done: () => done(),
|
|
88
|
+
title: spec.title,
|
|
89
|
+
headerRight: spec.headerRight,
|
|
90
|
+
items: spec.build(),
|
|
91
|
+
actions: spec.actions,
|
|
92
|
+
refresh: spec.build,
|
|
93
|
+
onSelect: spec.onSelect,
|
|
94
|
+
}),
|
|
95
|
+
{ overlay: true },
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Open a URL/file with the OS default handler (browser for http/.html). */
|
|
100
|
+
async function openExternally(pi: ExtensionAPI, target: string): Promise<void> {
|
|
101
|
+
const o = platform();
|
|
102
|
+
const r =
|
|
103
|
+
o === "darwin"
|
|
104
|
+
? await pi.exec("open", [target])
|
|
105
|
+
: o === "win32"
|
|
106
|
+
? await pi.exec("cmd", ["/c", "start", "", target])
|
|
107
|
+
: await pi.exec("xdg-open", [target]);
|
|
108
|
+
if (r.code !== 0) throw new Error(r.stderr || `open failed (exit ${r.code})`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Status marker for a rule: ● always-on · ◐ glob-matched · ○ disabled. */
|
|
112
|
+
function ruleMarker(r: RuleFile): string {
|
|
113
|
+
if (!r.enabled) return "○";
|
|
114
|
+
return r.meta.always || !(r.meta.globs && r.meta.globs.length) ? "●" : "◐";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Map a rule to a panel row (token estimate + globs + description preview). */
|
|
118
|
+
function ruleItem(r: RuleFile): ListItem {
|
|
119
|
+
const tokens = Math.ceil(r.body.length / 4);
|
|
120
|
+
const bits = [`${formatTok(tokens)} tok`];
|
|
121
|
+
if (r.meta.globs && r.meta.globs.length) bits.push(r.meta.globs.join(","));
|
|
122
|
+
if (!r.enabled) bits.push("off");
|
|
123
|
+
return {
|
|
124
|
+
id: r.relPath,
|
|
125
|
+
marker: ruleMarker(r),
|
|
126
|
+
label: r.relPath,
|
|
127
|
+
meta: bits.join(" · "),
|
|
128
|
+
body: r.meta.description ? `${r.meta.description}\n\n${r.body}` : r.body,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
68
132
|
export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
69
133
|
const {
|
|
70
134
|
getRules,
|
|
@@ -94,7 +158,8 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
94
158
|
confirm: (title, message) => ctx.ui.confirm(title, message),
|
|
95
159
|
};
|
|
96
160
|
const parts = args.trim().split(/\s+/);
|
|
97
|
-
|
|
161
|
+
// Bare `/rules` (empty args) opens the modal; an explicit subcommand does not.
|
|
162
|
+
const sub = parts[0] || "list";
|
|
98
163
|
const target = parts[1];
|
|
99
164
|
|
|
100
165
|
if (sub === "list") {
|
|
@@ -104,6 +169,22 @@ export function registerCommands(pi: ExtensionAPI, deps: CommandsDeps): void {
|
|
|
104
169
|
ui.notify("no rules loaded from any source", "info");
|
|
105
170
|
return;
|
|
106
171
|
}
|
|
172
|
+
// Rich modal in the TUI; plain select elsewhere (RPC/print).
|
|
173
|
+
if (ctx.mode === "tui") {
|
|
174
|
+
const analytics = analyzeRules(getRules(), CONTEXT_WINDOW_TOKENS);
|
|
175
|
+
await openListPanel(ctx, {
|
|
176
|
+
title: "soly · rules",
|
|
177
|
+
headerRight: `${getRules().length} rules · ${formatTok(analytics.totalTokens)} · ${analytics.contextBudgetPct.toFixed(1)}%`,
|
|
178
|
+
build: () => getRules().map(ruleItem),
|
|
179
|
+
actions: [
|
|
180
|
+
{ key: "e", hint: "enable", run: (it) => { const r = getRules().find((x) => x.relPath === it.id); if (r) r.enabled = true; } },
|
|
181
|
+
{ key: "d", hint: "disable", run: (it) => { const r = getRules().find((x) => x.relPath === it.id); if (r) r.enabled = false; } },
|
|
182
|
+
{ key: "r", hint: "reload", run: () => refreshRules() },
|
|
183
|
+
],
|
|
184
|
+
});
|
|
185
|
+
updateStatus(ui);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
107
188
|
const lines: string[] = [];
|
|
108
189
|
for (const r of rules) {
|
|
109
190
|
const status = r.enabled ? "●" : "○";
|
|
@@ -374,7 +455,34 @@ What must the LLM do?
|
|
|
374
455
|
confirm: (title, message) => ctx.ui.confirm(title, message),
|
|
375
456
|
};
|
|
376
457
|
const parts = args.trim().split(/\s+/);
|
|
377
|
-
|
|
458
|
+
// Bare `/docs` opens the modal; an explicit subcommand (e.g. stats) does not.
|
|
459
|
+
const sub = parts[0] || "list";
|
|
460
|
+
|
|
461
|
+
if (sub === "list") {
|
|
462
|
+
const docs = getIntentDocs();
|
|
463
|
+
if (docs.length === 0) {
|
|
464
|
+
ui.notify("no intent docs found in .soly/docs/ — drop your vision/domain docs there", "info");
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
if (ctx.mode === "tui") {
|
|
468
|
+
const total = docs.reduce((s, d) => s + d.tokens, 0);
|
|
469
|
+
await openListPanel(ctx, {
|
|
470
|
+
title: "soly · docs",
|
|
471
|
+
headerRight: `${docs.length} docs · ${formatTok(total)}`,
|
|
472
|
+
build: () =>
|
|
473
|
+
getIntentDocs().map((d) => ({
|
|
474
|
+
id: d.relPath,
|
|
475
|
+
marker: "○",
|
|
476
|
+
label: d.title || d.relPath,
|
|
477
|
+
meta: `${d.kind} · ${formatTok(d.tokens)} tok${d.oversized ? " · oversized" : ""}`,
|
|
478
|
+
body: d.preview,
|
|
479
|
+
})),
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
ui.notify(docs.map((d) => `○ ${d.title || d.relPath} (${formatTok(d.tokens)} tok)`).join("\n"), "info");
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
378
486
|
|
|
379
487
|
if (sub === "stats") {
|
|
380
488
|
const docs = getIntentDocs();
|
|
@@ -385,7 +493,7 @@ What must the LLM do?
|
|
|
385
493
|
}
|
|
386
494
|
|
|
387
495
|
ui.notify(
|
|
388
|
-
`Usage: /docs stats — show context breakdown
|
|
496
|
+
`Usage: /docs [list|stats] — open the docs panel, or show the context breakdown\n` +
|
|
389
497
|
`Found ${getIntentDocs().length} doc(s) loaded.`,
|
|
390
498
|
"info",
|
|
391
499
|
);
|
|
@@ -720,50 +828,158 @@ What must the LLM do?
|
|
|
720
828
|
},
|
|
721
829
|
};
|
|
722
830
|
|
|
831
|
+
const ICONS: Record<string, string> = {
|
|
832
|
+
position: "📍", state: "📄", plan: "📋", context: "💡",
|
|
833
|
+
research: "🔬", roadmap: "🗺️", progress: "📊",
|
|
834
|
+
phases: "📁", tasks: "✅", task: "🔎",
|
|
835
|
+
features: "⭐", milestone: "🎯", reload: "🔄", config: "⚙️",
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
// Short live preview shown in the modal's preview pane per subcommand.
|
|
839
|
+
const previewFor = (name: string): string => {
|
|
840
|
+
const s = getState();
|
|
841
|
+
const teaser = (p: string | null): string => (p ?? "").replace(/\s+/g, " ").slice(0, 240);
|
|
842
|
+
const phaseFile = (suffix: string) =>
|
|
843
|
+
s.currentPhase ? readIfExists(path.join(s.currentPhase.dir, `${s.currentPhase.slug}-${suffix}.md`)) : null;
|
|
844
|
+
switch (name) {
|
|
845
|
+
case "position": return s.position ? `${s.position.phase} · ${s.position.plan} · ${s.position.status} · ${s.progress.percent}%` : `${s.milestone} — no position set`;
|
|
846
|
+
case "progress": return `${s.progress.percent}% · ${s.progress.completedPhases}/${s.progress.totalPhases} phases · ${s.progress.completedPlans}/${s.progress.totalPlans} plans`;
|
|
847
|
+
case "phases": return s.phases.length ? s.phases.map((p) => `${p.number}.${p.name}`).join(" · ") : "no phases";
|
|
848
|
+
case "tasks": return s.tasks.length ? `${s.tasks.length} task(s) across ${s.features.length} feature(s)` : "no tasks";
|
|
849
|
+
case "features": return s.features.length ? s.features.map((f) => f.name).join(" · ") : "no features";
|
|
850
|
+
case "milestone": return s.milestone && s.milestone !== "—" ? String(s.milestoneName ?? s.milestone) : "no milestone set";
|
|
851
|
+
case "state": return teaser(s.stateBody) || "STATE.md not found";
|
|
852
|
+
case "roadmap": return teaser(s.roadmapBody) || "ROADMAP.md not found";
|
|
853
|
+
case "plan": return s.currentPlanPath ? teaser(readIfExists(s.currentPlanPath)) : "no current plan";
|
|
854
|
+
case "context": return s.currentPhase ? teaser(phaseFile("CONTEXT")) || "CONTEXT.md not found" : "no current phase";
|
|
855
|
+
case "research": return s.currentPhase ? teaser(phaseFile("RESEARCH")) || "RESEARCH.md not found" : "no current phase";
|
|
856
|
+
case "config": return "merged config — press ⏎ to view the JSON";
|
|
857
|
+
case "reload": return "re-read project state from disk";
|
|
858
|
+
default: return subcommands[name]?.description ?? "";
|
|
859
|
+
}
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const solyItems = (): ListItem[] =>
|
|
863
|
+
Object.keys(subcommands).map((name) => ({
|
|
864
|
+
id: name,
|
|
865
|
+
marker: ICONS[name] ?? "▸",
|
|
866
|
+
label: name,
|
|
867
|
+
meta: subcommands[name]!.description,
|
|
868
|
+
body: previewFor(name),
|
|
869
|
+
}));
|
|
870
|
+
|
|
871
|
+
// Plain-select fallback for non-TUI (RPC/print) modes.
|
|
723
872
|
const picker = async (label: string) => {
|
|
724
873
|
const entries = Object.entries(subcommands);
|
|
725
|
-
const lines = entries.map(
|
|
726
|
-
([name, spec], i) => {
|
|
727
|
-
const icons: Record<string, string> = {
|
|
728
|
-
position: "📍", state: "📄", plan: "📋", context: "💡",
|
|
729
|
-
research: "🔬", roadmap: "🗺️", progress: "📊",
|
|
730
|
-
phases: "📁", tasks: "✅", task: "🔎",
|
|
731
|
-
features: "⭐", milestone: "🎯", reload: "🔄",
|
|
732
|
-
config: "⚙️",
|
|
733
|
-
};
|
|
734
|
-
const icon = icons[name] ?? "▸";
|
|
735
|
-
return `${icon} ${name} — ${spec.description}`;
|
|
736
|
-
},
|
|
737
|
-
);
|
|
874
|
+
const lines = entries.map(([name, spec]) => `${ICONS[name] ?? "▸"} ${name} — ${spec.description}`);
|
|
738
875
|
const choice = await ui.select(label, lines);
|
|
739
876
|
if (choice != null && typeof choice === "number") {
|
|
740
877
|
const name = entries[choice]?.[0];
|
|
741
|
-
if (name)
|
|
742
|
-
await subcommands[name].run([name]);
|
|
743
|
-
}
|
|
878
|
+
if (name) await subcommands[name]!.run([name]);
|
|
744
879
|
}
|
|
745
880
|
};
|
|
746
881
|
|
|
882
|
+
const openMenu = async () => {
|
|
883
|
+
if (ctx.mode !== "tui") return picker("soly (esc to cancel):");
|
|
884
|
+
const s = getState();
|
|
885
|
+
await openListPanel(ctx, {
|
|
886
|
+
title: "soly · state",
|
|
887
|
+
headerRight: `${s.phases.length} phases · ${s.progress.percent}%`,
|
|
888
|
+
build: solyItems,
|
|
889
|
+
onSelect: (it) => {
|
|
890
|
+
void subcommands[it.id]?.run([it.id]);
|
|
891
|
+
},
|
|
892
|
+
});
|
|
893
|
+
};
|
|
894
|
+
|
|
747
895
|
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
748
896
|
const sub = parts[0] ?? "";
|
|
749
897
|
|
|
750
|
-
// /soly with no
|
|
751
|
-
if (!sub) {
|
|
752
|
-
return
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
if (sub === "help" || sub === "?" || sub === "--help" || sub === "-h") {
|
|
756
|
-
return picker("soly subcommand (esc to cancel):");
|
|
898
|
+
// /soly (or help) with no specific subcommand → the modal (TUI) / picker.
|
|
899
|
+
if (!sub || sub === "help" || sub === "?" || sub === "--help" || sub === "-h") {
|
|
900
|
+
return openMenu();
|
|
757
901
|
}
|
|
758
902
|
|
|
759
903
|
if (!subcommands[sub]) {
|
|
760
904
|
ui.notify(`soly: unknown subcommand '${sub}'`, "error");
|
|
761
|
-
return
|
|
905
|
+
return openMenu();
|
|
762
906
|
}
|
|
763
907
|
|
|
764
908
|
await subcommands[sub].run(parts);
|
|
765
909
|
},
|
|
766
910
|
});
|
|
911
|
+
// ============================================================================
|
|
912
|
+
// /artifacts — browse this session's html_artifact gallery
|
|
913
|
+
// ============================================================================
|
|
914
|
+
|
|
915
|
+
pi.registerCommand("artifacts", {
|
|
916
|
+
description: "browse this project's html_artifact gallery (list, open, clear)",
|
|
917
|
+
handler: async (args, ctx) => {
|
|
918
|
+
if (!getConfig().artifacts.server) {
|
|
919
|
+
ctx.ui.notify("soly: artifact server is disabled (artifacts.server=false)", "info");
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
// Always re-resolve: reuse another window's server or start ours, and
|
|
923
|
+
// re-probe a stale client whose owner may have died in the meantime.
|
|
924
|
+
let server = getArtifactServer();
|
|
925
|
+
try {
|
|
926
|
+
server = await ensureArtifactServer(artifactDir(getConfig().artifacts.dir, ctx.cwd), ctx.cwd);
|
|
927
|
+
} catch {
|
|
928
|
+
// ignore — handled by the count check below
|
|
929
|
+
}
|
|
930
|
+
if (!server || server.count === 0) {
|
|
931
|
+
ctx.ui.notify("soly: no artifacts for this project yet (use the html_artifact tool)", "info");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
const gallery = server.galleryUrl();
|
|
935
|
+
const sub = args.trim().split(/\s+/).filter(Boolean)[0] ?? "";
|
|
936
|
+
|
|
937
|
+
if (sub === "clear") {
|
|
938
|
+
const n = server.clear();
|
|
939
|
+
ctx.ui.notify(`soly: cleared ${n} artifact(s)`, "info");
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
if (sub === "open" || sub === "gallery") {
|
|
943
|
+
try {
|
|
944
|
+
await openExternally(pi, gallery);
|
|
945
|
+
ctx.ui.notify(`soly: opened ${gallery}`, "info");
|
|
946
|
+
} catch {
|
|
947
|
+
ctx.ui.notify(`soly artifacts gallery: ${gallery}`, "info");
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (ctx.mode === "tui") {
|
|
953
|
+
await openListPanel(ctx, {
|
|
954
|
+
title: "soly · artifacts",
|
|
955
|
+
// BMP-only header (no long URL — it overflowed the bar). Count only.
|
|
956
|
+
headerRight: `${server.count} artifact${server.count === 1 ? "" : "s"}`,
|
|
957
|
+
build: () =>
|
|
958
|
+
(getArtifactServer()?.list() ?? []).map((a) => ({
|
|
959
|
+
id: a.id,
|
|
960
|
+
marker: "▦", // BMP glyph — an astral emoji (🖼) breaks ListPanel width
|
|
961
|
+
label: a.title,
|
|
962
|
+
meta: new Date(a.createdAt).toLocaleTimeString(),
|
|
963
|
+
body: a.url,
|
|
964
|
+
})),
|
|
965
|
+
onSelect: (it) => {
|
|
966
|
+
const a = getArtifactServer()?.list().find((x) => x.id === it.id);
|
|
967
|
+
if (a) void openExternally(pi, a.url);
|
|
968
|
+
},
|
|
969
|
+
actions: [
|
|
970
|
+
{ key: "g", hint: "gallery", run: () => void openExternally(pi, gallery) },
|
|
971
|
+
{ key: "x", hint: "delete", run: (it) => { getArtifactServer()?.remove(it.id); } },
|
|
972
|
+
],
|
|
973
|
+
});
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// Non-TUI: print the list + gallery URL.
|
|
978
|
+
const lines = server.list().map((a) => `🖼 ${a.title} — ${a.url}`);
|
|
979
|
+
ctx.ui.notify([`soly artifacts gallery: ${gallery}`, "", ...lines].join("\n"), "info");
|
|
980
|
+
},
|
|
981
|
+
});
|
|
982
|
+
|
|
767
983
|
// ============================================================================
|
|
768
984
|
// /rulewizard
|
|
769
985
|
// ============================================================================
|
package/config.ts
CHANGED
|
@@ -35,11 +35,16 @@ export interface SolyConfig {
|
|
|
35
35
|
preferAskPro: boolean;
|
|
36
36
|
/** When soly pause is invoked, also auto-save HANDOFF.json (currently always true; knob for future). */
|
|
37
37
|
autoCheckpointOnPause: boolean;
|
|
38
|
-
/**
|
|
39
|
-
*
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
/** Show the soft "non-trivial / research" nudge as a chat box. Default off
|
|
39
|
+
* (it was noisy). The system-prompt nudge guides the LLM regardless. */
|
|
40
|
+
nudgeNotify: boolean;
|
|
41
|
+
/** Inject per-turn affordance hints (examples → html_artifact, options →
|
|
42
|
+
* decision_deck, …) when the prompt mentions those. Default on. */
|
|
43
|
+
toolHints: boolean;
|
|
44
|
+
/** For non-trivial tasks, tell the LLM to confirm the approach with the
|
|
45
|
+
* user (ask "ready to implement, or discuss first?") before writing code.
|
|
46
|
+
* Default on. */
|
|
47
|
+
confirmBeforeCode: boolean;
|
|
43
48
|
};
|
|
44
49
|
display: {
|
|
45
50
|
/** Always show the recommended (⭐) option as the first row. */
|
|
@@ -63,6 +68,48 @@ export interface SolyConfig {
|
|
|
63
68
|
/** $EDITOR command for opening files (e.g. "code", "vim", "cursor"). */
|
|
64
69
|
command: string;
|
|
65
70
|
};
|
|
71
|
+
chrome: {
|
|
72
|
+
/** Install soly's status chrome (top bar + custom footer + spinner). */
|
|
73
|
+
enabled: boolean;
|
|
74
|
+
/** Use ASCII fallbacks instead of Nerd-Font glyphs. */
|
|
75
|
+
ascii: boolean;
|
|
76
|
+
/** Working-spinner animation frames (rendered verbatim by pi). */
|
|
77
|
+
spinnerFrames: string[];
|
|
78
|
+
/** Working-spinner frame interval in ms. */
|
|
79
|
+
spinnerIntervalMs: number;
|
|
80
|
+
/** Show live telemetry (elapsed · ↑↓ tokens · tok/s) in the working line. */
|
|
81
|
+
telemetry: boolean;
|
|
82
|
+
/** Hex gradient stops for the welcome banner (empty → accent color). */
|
|
83
|
+
bannerColors: string[];
|
|
84
|
+
};
|
|
85
|
+
verify: {
|
|
86
|
+
/** Max self-review passes before the loop stops on its own. */
|
|
87
|
+
maxIterations: number;
|
|
88
|
+
/** Strip prior review iterations from context each pass ("fresh eyes"). */
|
|
89
|
+
freshContext: boolean;
|
|
90
|
+
/** Review prompt re-injected each pass. */
|
|
91
|
+
prompt: string;
|
|
92
|
+
/** Regex (case-insensitive) signaling "nothing left to fix" → exit. */
|
|
93
|
+
exitPatterns: string[];
|
|
94
|
+
/** Regex signaling issues were fixed → keep looping despite an exit phrase. */
|
|
95
|
+
issuesFixedPatterns: string[];
|
|
96
|
+
};
|
|
97
|
+
artifacts: {
|
|
98
|
+
/** Open generated HTML artifacts in the browser automatically. */
|
|
99
|
+
open: boolean;
|
|
100
|
+
/** Output directory. Empty → OS temp dir (pi-soly-artifacts/). Accepts an
|
|
101
|
+
* absolute path, ~, or a path relative to the project cwd. */
|
|
102
|
+
dir: string;
|
|
103
|
+
/** Serve artifacts from a per-session HTTP server with a live gallery
|
|
104
|
+
* (one stable localhost URL). When false, just open the .html file. */
|
|
105
|
+
server: boolean;
|
|
106
|
+
/** Path to a CSS file overriding the artifact theme. Empty → built-in.
|
|
107
|
+
* Resolved like `dir`; also auto-detected at `.soly/artifact-theme.css`. */
|
|
108
|
+
theme: string;
|
|
109
|
+
/** Prune session artifact dirs older than N days on session start.
|
|
110
|
+
* 0 = keep forever. */
|
|
111
|
+
retentionDays: number;
|
|
112
|
+
};
|
|
66
113
|
}
|
|
67
114
|
|
|
68
115
|
export const DEFAULT_CONFIG: SolyConfig = {
|
|
@@ -75,7 +122,9 @@ export const DEFAULT_CONFIG: SolyConfig = {
|
|
|
75
122
|
agent: {
|
|
76
123
|
preferAskPro: true,
|
|
77
124
|
autoCheckpointOnPause: true,
|
|
78
|
-
|
|
125
|
+
nudgeNotify: false,
|
|
126
|
+
toolHints: true,
|
|
127
|
+
confirmBeforeCode: true,
|
|
79
128
|
},
|
|
80
129
|
display: {
|
|
81
130
|
defaultRecommendedFirst: true,
|
|
@@ -92,6 +141,37 @@ export const DEFAULT_CONFIG: SolyConfig = {
|
|
|
92
141
|
editor: {
|
|
93
142
|
command: "code",
|
|
94
143
|
},
|
|
144
|
+
chrome: {
|
|
145
|
+
enabled: true,
|
|
146
|
+
ascii: false,
|
|
147
|
+
spinnerFrames: [
|
|
148
|
+
"⠁", "⠉", "⠙", "⠹", "⠽", "⠿", "⠾", "⠼", "⠶", "⠦", "⠆", "⠂",
|
|
149
|
+
"◴", "◷", "◶", "◵",
|
|
150
|
+
"⠁", "⠉", "⠙", "⠹", "⠽", "⠿", "⠾", "⠼", "⠶", "⠦", "⠆", "⠂",
|
|
151
|
+
],
|
|
152
|
+
spinnerIntervalMs: 150,
|
|
153
|
+
telemetry: true,
|
|
154
|
+
bannerColors: [], // empty → gradient derived from the theme accent
|
|
155
|
+
},
|
|
156
|
+
verify: {
|
|
157
|
+
maxIterations: 7,
|
|
158
|
+
freshContext: false,
|
|
159
|
+
prompt:
|
|
160
|
+
"Review the work from this session with fresh eyes. Re-read any relevant plan, " +
|
|
161
|
+
"spec, or PRD first. Look for bugs, missed edge cases, missing error handling, and " +
|
|
162
|
+
"inconsistencies with the project rules. If you find problems, fix them, then end your " +
|
|
163
|
+
'reply with: "Fixed N issue(s). Ready for another review." If you find nothing, end with ' +
|
|
164
|
+
'exactly: "No issues found."',
|
|
165
|
+
exitPatterns: ["no issues found", "no further issues", "nothing to fix"],
|
|
166
|
+
issuesFixedPatterns: ["fixed \\d+ issue", "ready for another review", "issues? fixed"],
|
|
167
|
+
},
|
|
168
|
+
artifacts: {
|
|
169
|
+
open: true,
|
|
170
|
+
dir: "",
|
|
171
|
+
server: true,
|
|
172
|
+
theme: "",
|
|
173
|
+
retentionDays: 7,
|
|
174
|
+
},
|
|
95
175
|
};
|
|
96
176
|
|
|
97
177
|
/** Raw parsed shape from a config.json — could be partial, could have wrong types. */
|
|
@@ -130,8 +210,12 @@ function deepMerge(base: SolyConfig, over: RawConfig): SolyConfig {
|
|
|
130
210
|
merged.agent.preferAskPro = over.agent.preferAskPro;
|
|
131
211
|
if (typeof over.agent.autoCheckpointOnPause === "boolean")
|
|
132
212
|
merged.agent.autoCheckpointOnPause = over.agent.autoCheckpointOnPause;
|
|
133
|
-
if (typeof over.agent.
|
|
134
|
-
merged.agent.
|
|
213
|
+
if (typeof over.agent.nudgeNotify === "boolean")
|
|
214
|
+
merged.agent.nudgeNotify = over.agent.nudgeNotify;
|
|
215
|
+
if (typeof over.agent.toolHints === "boolean")
|
|
216
|
+
merged.agent.toolHints = over.agent.toolHints;
|
|
217
|
+
if (typeof over.agent.confirmBeforeCode === "boolean")
|
|
218
|
+
merged.agent.confirmBeforeCode = over.agent.confirmBeforeCode;
|
|
135
219
|
}
|
|
136
220
|
if (over.display) {
|
|
137
221
|
if (typeof over.display.defaultRecommendedFirst === "boolean")
|
|
@@ -153,6 +237,38 @@ function deepMerge(base: SolyConfig, over: RawConfig): SolyConfig {
|
|
|
153
237
|
if (over.editor && typeof over.editor.command === "string") {
|
|
154
238
|
merged.editor.command = over.editor.command;
|
|
155
239
|
}
|
|
240
|
+
if (over.chrome) {
|
|
241
|
+
if (typeof over.chrome.enabled === "boolean") merged.chrome.enabled = over.chrome.enabled;
|
|
242
|
+
if (typeof over.chrome.ascii === "boolean") merged.chrome.ascii = over.chrome.ascii;
|
|
243
|
+
if (Array.isArray(over.chrome.spinnerFrames)) {
|
|
244
|
+
const frames = over.chrome.spinnerFrames.filter((f) => typeof f === "string");
|
|
245
|
+
if (frames.length > 0) merged.chrome.spinnerFrames = frames;
|
|
246
|
+
}
|
|
247
|
+
if (typeof over.chrome.spinnerIntervalMs === "number" && over.chrome.spinnerIntervalMs > 0)
|
|
248
|
+
merged.chrome.spinnerIntervalMs = over.chrome.spinnerIntervalMs;
|
|
249
|
+
if (typeof over.chrome.telemetry === "boolean") merged.chrome.telemetry = over.chrome.telemetry;
|
|
250
|
+
if (Array.isArray(over.chrome.bannerColors)) {
|
|
251
|
+
merged.chrome.bannerColors = over.chrome.bannerColors.filter((c) => typeof c === "string");
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (over.verify) {
|
|
255
|
+
if (typeof over.verify.maxIterations === "number" && over.verify.maxIterations > 0)
|
|
256
|
+
merged.verify.maxIterations = over.verify.maxIterations;
|
|
257
|
+
if (typeof over.verify.freshContext === "boolean") merged.verify.freshContext = over.verify.freshContext;
|
|
258
|
+
if (typeof over.verify.prompt === "string" && over.verify.prompt.trim()) merged.verify.prompt = over.verify.prompt;
|
|
259
|
+
if (Array.isArray(over.verify.exitPatterns))
|
|
260
|
+
merged.verify.exitPatterns = over.verify.exitPatterns.filter((p) => typeof p === "string");
|
|
261
|
+
if (Array.isArray(over.verify.issuesFixedPatterns))
|
|
262
|
+
merged.verify.issuesFixedPatterns = over.verify.issuesFixedPatterns.filter((p) => typeof p === "string");
|
|
263
|
+
}
|
|
264
|
+
if (over.artifacts) {
|
|
265
|
+
if (typeof over.artifacts.open === "boolean") merged.artifacts.open = over.artifacts.open;
|
|
266
|
+
if (typeof over.artifacts.dir === "string") merged.artifacts.dir = over.artifacts.dir;
|
|
267
|
+
if (typeof over.artifacts.server === "boolean") merged.artifacts.server = over.artifacts.server;
|
|
268
|
+
if (typeof over.artifacts.theme === "string") merged.artifacts.theme = over.artifacts.theme;
|
|
269
|
+
if (typeof over.artifacts.retentionDays === "number" && over.artifacts.retentionDays >= 0)
|
|
270
|
+
merged.artifacts.retentionDays = over.artifacts.retentionDays;
|
|
271
|
+
}
|
|
156
272
|
return merged;
|
|
157
273
|
}
|
|
158
274
|
|