pi-monofold 0.5.0 → 0.6.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 +6 -0
- package/index.ts +154 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -106,6 +106,12 @@ Example command flows: [docs/examples.md](./docs/examples.md).
|
|
|
106
106
|
|
|
107
107
|
Default focus shortcut: `ctrl+shift+m` cycles Active Focus forward through `focusPresets` YAML order. No backward focus shortcut ships in the MVP.
|
|
108
108
|
|
|
109
|
+
When Active Focus is set, Pi Monofold injects the active preset's `contextFiles` into each agent turn under **Focus Context Injection** and recomposes the manifest so active Workspace Targets are shown first while non-active targets are collapsed to one-line summaries. The MVP uses provisional context-injection caps that are intentionally temporary and exposed as constants for future tuning:
|
|
110
|
+
|
|
111
|
+
- Max **6** context files per active preset.
|
|
112
|
+
- Max **6,000** characters per file, with `… [truncated]` appended when a file is cut.
|
|
113
|
+
- Max **12,000** injected file-content characters per turn; remaining files are skipped and a warning is surfaced once for that turn.
|
|
114
|
+
|
|
109
115
|
Agent tools (`monofold_list`, `monofold_read`, `monofold_write`, `monofold_git`, `monofold_init`) sit behind these commands. Full reference: [docs/usage.md](./docs/usage.md).
|
|
110
116
|
|
|
111
117
|
## Safe read defaults
|
package/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
findFocusPresetById,
|
|
13
13
|
getActiveFocusPresetId,
|
|
14
14
|
getActiveFocusPresetPosition,
|
|
15
|
+
matchesFocusTarget,
|
|
15
16
|
parseFocusPresets,
|
|
16
17
|
setActiveFocusPresetByLabel,
|
|
17
18
|
setActiveFocusPresetId,
|
|
@@ -170,6 +171,20 @@ const ROUTE_KEYS = new Set(["path", "filenameTemplate", "metadata"]);
|
|
|
170
171
|
const FOCUS_STATUS_ID = "monofold-focus";
|
|
171
172
|
const FOCUS_CYCLE_SHORTCUT = "ctrl+shift+m";
|
|
172
173
|
const FOCUS_CYCLE_ACTION_ID = "app.monofold.focus.cycleForward";
|
|
174
|
+
export const FOCUS_CONTEXT_MAX_FILES = 6;
|
|
175
|
+
export const FOCUS_CONTEXT_MAX_CHARS_PER_FILE = 6_000;
|
|
176
|
+
export const FOCUS_CONTEXT_MAX_TOTAL_CHARS = 12_000;
|
|
177
|
+
const FOCUS_CONTEXT_TRUNCATION_MARKER = "… [truncated]";
|
|
178
|
+
|
|
179
|
+
type ActiveFocusWorkspace = {
|
|
180
|
+
workspace: ResolvedWorkspace;
|
|
181
|
+
targetIndex: number;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
type FocusContextInjection = {
|
|
185
|
+
text: string;
|
|
186
|
+
totalCapReached: boolean;
|
|
187
|
+
};
|
|
173
188
|
|
|
174
189
|
function normalizeSlashes(value: string): string {
|
|
175
190
|
return value.replace(/\\/g, "/");
|
|
@@ -921,6 +936,42 @@ function formatWorkspaceLabel(workspace: ResolvedWorkspace): string {
|
|
|
921
936
|
return `${workspace.targetId} ${displayName}[${workspace.tags.join(", ")}] ${workspace.displayPath}${parent}`;
|
|
922
937
|
}
|
|
923
938
|
|
|
939
|
+
function formatCollapsedWorkspaceLine(workspace: ResolvedWorkspace): string {
|
|
940
|
+
const label = workspace.name ?? "(unnamed)";
|
|
941
|
+
return `${workspace.targetId} ${label} [${workspace.tags.join(", ")}] ${workspace.displayPath}`;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
function getActiveFocusPreset(loaded: LoadedConfig): FocusPreset | undefined {
|
|
945
|
+
const activeId = getActiveFocusPresetId();
|
|
946
|
+
return activeId ? findFocusPresetById(loaded.raw.focusPresets, activeId) : undefined;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function getActiveFocusWorkspaces(loaded: LoadedConfig, preset: FocusPreset): ActiveFocusWorkspace[] {
|
|
950
|
+
const active: ActiveFocusWorkspace[] = [];
|
|
951
|
+
const seenTargets = new Set<string>();
|
|
952
|
+
for (let targetIndex = 0; targetIndex < preset.targets.length; targetIndex += 1) {
|
|
953
|
+
const target = preset.targets[targetIndex]!;
|
|
954
|
+
for (const workspace of loaded.workspaces) {
|
|
955
|
+
if (seenTargets.has(workspace.targetId)) continue;
|
|
956
|
+
if (!matchesFocusTarget(workspace, target.targetTags)) continue;
|
|
957
|
+
seenTargets.add(workspace.targetId);
|
|
958
|
+
active.push({ workspace, targetIndex });
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
return active;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function truncateWithMarker(value: string, maxChars: number): { text: string; truncated: boolean } {
|
|
965
|
+
if (value.length <= maxChars) return { text: value, truncated: false };
|
|
966
|
+
if (maxChars <= FOCUS_CONTEXT_TRUNCATION_MARKER.length) {
|
|
967
|
+
return { text: FOCUS_CONTEXT_TRUNCATION_MARKER.slice(0, maxChars), truncated: true };
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
text: value.slice(0, maxChars - FOCUS_CONTEXT_TRUNCATION_MARKER.length) + FOCUS_CONTEXT_TRUNCATION_MARKER,
|
|
971
|
+
truncated: true,
|
|
972
|
+
};
|
|
973
|
+
}
|
|
974
|
+
|
|
924
975
|
function relativePath(workspace: ResolvedWorkspace, inputPath: string): string {
|
|
925
976
|
assertWorkspaceInternalRelative("path", inputPath);
|
|
926
977
|
return path.join(workspace.resolvedPath, inputPath);
|
|
@@ -939,22 +990,104 @@ async function gitRoot(workspace: ResolvedWorkspace): Promise<string | undefined
|
|
|
939
990
|
return undefined;
|
|
940
991
|
}
|
|
941
992
|
|
|
993
|
+
async function buildFullWorkspaceManifestEntry(workspace: ResolvedWorkspace, suffix = ""): Promise<string> {
|
|
994
|
+
const git = await gitSummary(workspace).catch((error) => ({ isGit: false, status: `git status error: ${String(error)}` }));
|
|
995
|
+
return (
|
|
996
|
+
`- ${formatWorkspaceLabel(workspace)}${suffix}\n` +
|
|
997
|
+
` capabilities: ${workspace.capabilities.join(", ")}\n` +
|
|
998
|
+
` routes: ${Object.keys(workspace.normalizedRoutes).join(", ") || "none"}\n` +
|
|
999
|
+
` contextFiles: ${workspace.effectiveContextFiles.join(", ") || "none"}\n` +
|
|
1000
|
+
` git: ${git.isGit ? git.status : "not a git repository"}`
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
|
|
942
1004
|
async function buildManifest(loaded: LoadedConfig): Promise<string> {
|
|
943
1005
|
const lines = ["Pi Monofold Manifest:"];
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
1006
|
+
const activePreset = getActiveFocusPreset(loaded);
|
|
1007
|
+
if (activePreset) {
|
|
1008
|
+
const activeWorkspaces = getActiveFocusWorkspaces(loaded, activePreset);
|
|
1009
|
+
const activeTargetIds = new Set(activeWorkspaces.map(({ workspace }) => workspace.targetId));
|
|
1010
|
+
if (activeWorkspaces.length > 0) {
|
|
1011
|
+
lines.push(`Active Focus: ${activePreset.label} (${activePreset.id})`);
|
|
1012
|
+
for (const { workspace } of activeWorkspaces) {
|
|
1013
|
+
lines.push(await buildFullWorkspaceManifestEntry(workspace, " (active)"));
|
|
1014
|
+
}
|
|
1015
|
+
const collapsed = loaded.workspaces.filter((workspace) => !activeTargetIds.has(workspace.targetId));
|
|
1016
|
+
if (collapsed.length > 0) {
|
|
1017
|
+
lines.push("Non-active Workspace Targets (collapsed):");
|
|
1018
|
+
for (const workspace of collapsed) lines.push(`- ${formatCollapsedWorkspaceLine(workspace)}`);
|
|
1019
|
+
}
|
|
1020
|
+
lines.push("Use monofold_* tools for cross-workspace operations. Do not guess output paths when a route exists.");
|
|
1021
|
+
return lines.join("\n");
|
|
1022
|
+
}
|
|
953
1023
|
}
|
|
1024
|
+
for (const workspace of loaded.workspaces) lines.push(await buildFullWorkspaceManifestEntry(workspace));
|
|
954
1025
|
lines.push("Use monofold_* tools for cross-workspace operations. Do not guess output paths when a route exists.");
|
|
955
1026
|
return lines.join("\n");
|
|
956
1027
|
}
|
|
957
1028
|
|
|
1029
|
+
async function buildFocusContextInjection(loaded: LoadedConfig, preset: FocusPreset): Promise<FocusContextInjection> {
|
|
1030
|
+
const activeWorkspaces = getActiveFocusWorkspaces(loaded, preset);
|
|
1031
|
+
const lines = ["## Focus Context Injection", "", `Active Focus: ${preset.label} (${preset.id})`];
|
|
1032
|
+
const notices: string[] = [];
|
|
1033
|
+
const seenFiles = new Set<string>();
|
|
1034
|
+
let injectedFileCount = 0;
|
|
1035
|
+
let totalInjectedChars = 0;
|
|
1036
|
+
let skippedByFileCap = 0;
|
|
1037
|
+
let skippedByTotalCap = 0;
|
|
1038
|
+
let totalCapReached = false;
|
|
1039
|
+
|
|
1040
|
+
for (const { workspace } of activeWorkspaces) {
|
|
1041
|
+
for (const contextFile of workspace.effectiveContextFiles) {
|
|
1042
|
+
const absolutePath = relativePath(workspace, contextFile);
|
|
1043
|
+
const dedupeKey = normalizeGuardPath(absolutePath);
|
|
1044
|
+
if (seenFiles.has(dedupeKey)) continue;
|
|
1045
|
+
seenFiles.add(dedupeKey);
|
|
1046
|
+
|
|
1047
|
+
if (injectedFileCount >= FOCUS_CONTEXT_MAX_FILES) {
|
|
1048
|
+
skippedByFileCap += 1;
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
let raw: string;
|
|
1053
|
+
try {
|
|
1054
|
+
raw = await readFile(absolutePath, "utf8");
|
|
1055
|
+
} catch (error) {
|
|
1056
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1057
|
+
notices.push(`Skipped unreadable context file ${workspace.targetId}:${contextFile} (${message})`);
|
|
1058
|
+
continue;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const perFile = truncateWithMarker(raw, FOCUS_CONTEXT_MAX_CHARS_PER_FILE);
|
|
1062
|
+
if (perFile.truncated) notices.push(`Truncated ${workspace.targetId}:${contextFile} to ${FOCUS_CONTEXT_MAX_CHARS_PER_FILE} characters`);
|
|
1063
|
+
|
|
1064
|
+
if (totalInjectedChars + perFile.text.length > FOCUS_CONTEXT_MAX_TOTAL_CHARS) {
|
|
1065
|
+
skippedByTotalCap += 1;
|
|
1066
|
+
totalCapReached = true;
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
injectedFileCount += 1;
|
|
1071
|
+
totalInjectedChars += perFile.text.length;
|
|
1072
|
+
lines.push(
|
|
1073
|
+
"",
|
|
1074
|
+
`### ${workspace.targetId} ${workspace.name ?? "(unnamed)"}: ${contextFile}`,
|
|
1075
|
+
`Workspace Target: ${formatWorkspaceLabel(workspace)}`,
|
|
1076
|
+
"```text",
|
|
1077
|
+
perFile.text,
|
|
1078
|
+
"```",
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
if (skippedByFileCap > 0) notices.push(`Skipped ${skippedByFileCap} context file(s) after the ${FOCUS_CONTEXT_MAX_FILES}-file cap`);
|
|
1084
|
+
if (skippedByTotalCap > 0) notices.push(`Skipped ${skippedByTotalCap} context file(s) after the ${FOCUS_CONTEXT_MAX_TOTAL_CHARS}-character total cap`);
|
|
1085
|
+
if (injectedFileCount === 0) notices.push("No focus context files were injected for the active preset");
|
|
1086
|
+
if (notices.length > 0) lines.splice(3, 0, "", "Notices:", ...notices.map((notice) => `- ${notice}`));
|
|
1087
|
+
|
|
1088
|
+
return { text: lines.join("\n"), totalCapReached };
|
|
1089
|
+
}
|
|
1090
|
+
|
|
958
1091
|
function slugify(title: string): string {
|
|
959
1092
|
const normalized = title
|
|
960
1093
|
.trim()
|
|
@@ -1079,7 +1212,16 @@ export default function piMultiWorkspace(pi: ExtensionAPI) {
|
|
|
1079
1212
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
1080
1213
|
try {
|
|
1081
1214
|
const loaded = await loadConfig(ctx.cwd);
|
|
1215
|
+
ensureActiveFocusInitialized(loaded.raw.focusPresets);
|
|
1082
1216
|
const manifest = await buildManifest(loaded);
|
|
1217
|
+
const activePreset = getActiveFocusPreset(loaded);
|
|
1218
|
+
const focusInjection = activePreset ? await buildFocusContextInjection(loaded, activePreset) : undefined;
|
|
1219
|
+
if (focusInjection?.totalCapReached && ctx.hasUI) {
|
|
1220
|
+
ctx.ui.notify(
|
|
1221
|
+
`Focus Context Injection reached the ${FOCUS_CONTEXT_MAX_TOTAL_CHARS}-character total cap; skipped remaining context files.`,
|
|
1222
|
+
"warning",
|
|
1223
|
+
);
|
|
1224
|
+
}
|
|
1083
1225
|
return {
|
|
1084
1226
|
systemPrompt:
|
|
1085
1227
|
_event.systemPrompt +
|
|
@@ -1088,6 +1230,9 @@ export default function piMultiWorkspace(pi: ExtensionAPI) {
|
|
|
1088
1230
|
## Pi Monofold
|
|
1089
1231
|
|
|
1090
1232
|
${manifest}
|
|
1233
|
+
${focusInjection ? `
|
|
1234
|
+
|
|
1235
|
+
${focusInjection.text}` : ""}
|
|
1091
1236
|
`,
|
|
1092
1237
|
};
|
|
1093
1238
|
} catch {
|
package/package.json
CHANGED