pi-monofold 0.8.0 → 0.9.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 CHANGED
@@ -86,7 +86,7 @@ pi -e npm:pi-monofold
86
86
  1. Install the extension (see [Install](#install)).
87
87
  2. In your control repository, create `.pi/monofold.yaml` with at least one workspace entry (or run `/monofold:init`).
88
88
  3. Start Pi in the control repository and run `/monofold:explore show the project workspaces`.
89
- 4. Use `/monofold:focus` or `ctrl+shift+m` to switch focus presets when `focusPresets` are configured.
89
+ 4. Use `/monofold:focus`, `ctrl+shift+m`, or `shift+ctrl+f` to switch focus presets when `focusPresets` are configured.
90
90
  5. Use `/monofold:write` for routed Markdown outputs and `/monofold:git` for guarded git workflows.
91
91
 
92
92
  Example command flows: [docs/examples.md](./docs/examples.md).
@@ -100,11 +100,12 @@ Example command flows: [docs/examples.md](./docs/examples.md).
100
100
  | `/monofold:config` | Add or change workspaces and project workspaces |
101
101
  | `/monofold:git` | Run guarded git status, commit, push, or commit+push |
102
102
  | `/monofold:focus` | Select the active focus preset from a TUI list |
103
+ | `/monofold:focus-prev` | Cycle Active Focus backward through `focusPresets` YAML order |
103
104
  | `/monofold:guide` | Interactive guide for common flows |
104
105
  | `/monofold:init` | Create or update `.pi/monofold.yaml` |
105
106
  | `/monofold:update` | Migrate legacy config and optionally request config edits |
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
+ Default focus shortcuts: `ctrl+shift+m` cycles Active Focus forward and `shift+ctrl+f` cycles backward through `focusPresets` YAML order. Both wrap at the start/end of the list.
108
109
 
109
110
  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. Tag-based target inference in `monofold_read`, `monofold_write`, and `monofold_git` also prefers Workspace Targets that belong to the active preset when a tag query would otherwise match multiple candidates; explicit `targetId` / workspace name selectors and uniquely matching targets are unchanged. If multiple in-focus targets still tie, the existing workspace selection flow applies. The MVP uses provisional context-injection caps that are intentionally temporary and exposed as constants for future tuning:
110
111
 
package/focus-preset.ts CHANGED
@@ -186,6 +186,25 @@ export function cycleActiveFocusPresetForward(focusPresets: FocusPreset[] | unde
186
186
  return { preset, index: nextIndex, total: presets.length, changed };
187
187
  }
188
188
 
189
+ /** Cycles the active focus preset backward in YAML order. */
190
+ export function cycleActiveFocusPresetBackward(focusPresets: FocusPreset[] | undefined): FocusCycleResult | null {
191
+ const presets = focusPresets ?? [];
192
+ if (presets.length === 0) {
193
+ clearActiveFocusPresetId();
194
+ return null;
195
+ }
196
+
197
+ ensureActiveFocusInitialized(presets);
198
+ const currentId = getActiveFocusPresetId();
199
+ const currentIndex = currentId ? presets.findIndex((preset) => preset.id === currentId) : -1;
200
+ const nextIndex =
201
+ currentIndex < 0 ? presets.length - 1 : (currentIndex - 1 + presets.length) % presets.length;
202
+ const preset = presets[nextIndex]!;
203
+ const changed = presets.length > 1 && preset.id !== currentId;
204
+ setActiveFocusPresetId(preset.id, presets);
205
+ return { preset, index: nextIndex, total: presets.length, changed };
206
+ }
207
+
189
208
  /** Clears the active focus preset for the current process/session. */
190
209
  export function clearActiveFocusPresetId(): void {
191
210
  activeFocusPresetId = null;
package/index.ts CHANGED
@@ -6,6 +6,7 @@ import path from "node:path";
6
6
  import YAML from "yaml";
7
7
  import {
8
8
  type ActiveFocusPresetPosition,
9
+ cycleActiveFocusPresetBackward,
9
10
  cycleActiveFocusPresetForward,
10
11
  type FocusPreset,
11
12
  ensureActiveFocusInitialized,
@@ -181,6 +182,8 @@ const ROUTE_KEYS = new Set(["path", "filenameTemplate", "metadata"]);
181
182
  const FOCUS_STATUS_ID = "monofold-focus";
182
183
  const FOCUS_CYCLE_SHORTCUT = "ctrl+shift+m";
183
184
  const FOCUS_CYCLE_ACTION_ID = "app.monofold.focus.cycleForward";
185
+ const FOCUS_CYCLE_BACKWARD_SHORTCUT = "shift+ctrl+f";
186
+ const FOCUS_CYCLE_BACKWARD_ACTION_ID = "app.monofold.focus.cycleBackward";
184
187
  export const FOCUS_CONTEXT_MAX_FILES = 6;
185
188
  export const FOCUS_CONTEXT_MAX_CHARS_PER_FILE = 6_000;
186
189
  export const FOCUS_CONTEXT_MAX_TOTAL_CHARS = 12_000;
@@ -896,7 +899,7 @@ function sendCommandError(pi: ExtensionAPI, command: string, error: unknown, usa
896
899
  }
897
900
 
898
901
  function formatFocusStatus(position: ActiveFocusPresetPosition): string {
899
- const base = `focus: ${position.preset.label} (${position.index + 1}/${position.total}) ${FOCUS_CYCLE_SHORTCUT}`;
902
+ const base = `focus: ${position.preset.label} (${position.index + 1}/${position.total}) ${FOCUS_CYCLE_SHORTCUT} / ${FOCUS_CYCLE_BACKWARD_SHORTCUT}`;
900
903
  if (position.preset.defaultRouteOverride) {
901
904
  return `${base} route:${position.preset.defaultRouteOverride}`;
902
905
  }
@@ -1902,11 +1905,38 @@ ${focusInjection.text}` : ""}
1902
1905
  }
1903
1906
  };
1904
1907
 
1908
+ const cycleFocusBackward = async (ctx: ExtensionContext) => {
1909
+ try {
1910
+ const loaded = await loadConfig(ctx.cwd);
1911
+ const focusPresets = loaded.raw.focusPresets ?? [];
1912
+ if (focusPresets.length === 0) {
1913
+ notifyNoFocusPresets(ctx);
1914
+ return;
1915
+ }
1916
+ const result = cycleActiveFocusPresetBackward(focusPresets);
1917
+ if (!result) return;
1918
+ updateFocusStatus(ctx, loaded);
1919
+ warnZeroTargetMatchesForPreset(result.preset, loaded.workspaces, (message) => ctx.ui.notify(message, "warning"));
1920
+ if (focusPresets.length === 1) {
1921
+ ctx.ui.notify(`Active Focus unchanged: ${result.preset.label} (1/1)`, "info");
1922
+ return;
1923
+ }
1924
+ ctx.ui.notify(`Active Focus: ${result.preset.label} (${result.index + 1}/${result.total})`, "info");
1925
+ } catch (error) {
1926
+ const message = error instanceof Error ? error.message : String(error);
1927
+ ctx.ui.notify(`Monofold focus cycle failed: ${message}`, "error");
1928
+ }
1929
+ };
1930
+
1905
1931
  pi.registerCommand("monofold:explore", { description: "Explore configured workspaces via natural-language handoff", handler: intentCommand("Explore") });
1906
1932
  pi.registerCommand("monofold:write", { description: "Create routed Markdown via natural-language handoff", handler: intentCommand("Write") });
1907
1933
  pi.registerCommand("monofold:config", { description: "Change Workspace configuration via natural-language handoff", handler: intentCommand("Config") });
1908
1934
  pi.registerCommand("monofold:git", { description: "Run workspace git workflows via natural-language handoff", handler: intentCommand("Git") });
1909
1935
  pi.registerCommand("monofold:focus", { description: "Select the active Monofold focus preset from a TUI list", handler: focusCommand });
1936
+ pi.registerCommand("monofold:focus-prev", {
1937
+ description: "Cycle the active Monofold focus preset backward through focusPresets YAML order",
1938
+ handler: (_args, ctx) => cycleFocusBackward(ctx),
1939
+ });
1910
1940
  pi.registerCommand("monofold:guide", { description: "Conversational guide for Pi Monofold workflows", handler: guideCommand });
1911
1941
  pi.registerCommand("monofold:update", { description: `Migrate and validate ${CONFIG_RELATIVE_PATH}`, handler: updateCommand });
1912
1942
  pi.registerCommand("monofold:clear-unknown-path-allows", {
@@ -1919,6 +1949,11 @@ ${focusInjection.text}` : ""}
1919
1949
  handler: cycleFocusForward,
1920
1950
  });
1921
1951
 
1952
+ pi.registerShortcut(FOCUS_CYCLE_BACKWARD_SHORTCUT, {
1953
+ description: `${FOCUS_CYCLE_BACKWARD_ACTION_ID}: cycle active Monofold focus preset backward`,
1954
+ handler: cycleFocusBackward,
1955
+ });
1956
+
1922
1957
 
1923
1958
  pi.registerCommand("monofold:list", { description: "List configured Pi Monofold workspaces (legacy)", handler: listCommand });
1924
1959
 
package/package.json CHANGED
@@ -1,65 +1,66 @@
1
- {
2
- "scripts": {
3
- "typecheck": "tsc --noEmit",
4
- "test": "node --import tsx --test tests/**/*.test.ts",
5
- "check": "npm run typecheck && npm test && npm pack --dry-run"
6
- },
7
- "peerDependencies": {
8
- "typebox": "*",
9
- "@earendil-works/pi-coding-agent": "*",
10
- "@earendil-works/pi-ai": "*"
11
- },
12
- "description": "Pi extension that folds multiple repositories and folders into a guarded virtual monorepo for AI agents.",
13
- "type": "module",
14
- "version": "0.8.0",
15
- "pi": {
16
- "extensions": [
17
- "./index.ts"
18
- ]
19
- },
20
- "files": [
21
- "README.md",
22
- "LICENSE",
23
- "index.ts",
24
- "path-normalize.ts",
25
- "unknown-path-allows.ts",
26
- "file-read-preview.ts",
27
- "focus-preset.ts",
28
- "focus-route-override.ts",
29
- "focus-skills.ts",
30
- "monofold-read-ops.ts",
31
- "read-caps.ts",
32
- "validation.ts"
33
- ],
34
- "name": "pi-monofold",
35
- "devDependencies": {
36
- "typescript": "^6.0.3",
37
- "typebox": "^1.1.38",
38
- "@types/node": "^25.9.1",
39
- "tsx": "^4.20.5",
40
- "@earendil-works/pi-coding-agent": "^0.75.4",
41
- "@earendil-works/pi-ai": "^0.75.4"
42
- },
43
- "dependencies": {
44
- "yaml": "^2.8.1"
45
- },
46
- "keywords": [
47
- "pi-package",
48
- "pi-extension",
49
- "ai-coding",
50
- "virtual-monorepo",
51
- "multi-repo"
52
- ],
53
- "repository": {
54
- "type": "git",
55
- "url": "git+https://github.com/eiei114/pi-monofold.git"
56
- },
57
- "bugs": {
58
- "url": "https://github.com/eiei114/pi-monofold/issues"
59
- },
60
- "homepage": "https://github.com/eiei114/pi-monofold#readme",
61
- "publishConfig": {
62
- "access": "public"
63
- },
64
- "license": "MIT"
65
- }
1
+ {
2
+ "scripts": {
3
+ "typecheck": "tsc --noEmit",
4
+ "test": "node --import tsx --test tests/**/*.test.ts",
5
+ "check": "npm run typecheck && npm test && npm pack --dry-run",
6
+ "version:check": "node scripts/check-version-bump.mjs"
7
+ },
8
+ "peerDependencies": {
9
+ "typebox": "*",
10
+ "@earendil-works/pi-coding-agent": "*",
11
+ "@earendil-works/pi-ai": "*"
12
+ },
13
+ "description": "Pi extension that folds multiple repositories and folders into a guarded virtual monorepo for AI agents.",
14
+ "type": "module",
15
+ "version": "0.9.0",
16
+ "pi": {
17
+ "extensions": [
18
+ "./index.ts"
19
+ ]
20
+ },
21
+ "files": [
22
+ "README.md",
23
+ "LICENSE",
24
+ "index.ts",
25
+ "path-normalize.ts",
26
+ "unknown-path-allows.ts",
27
+ "file-read-preview.ts",
28
+ "focus-preset.ts",
29
+ "focus-route-override.ts",
30
+ "focus-skills.ts",
31
+ "monofold-read-ops.ts",
32
+ "read-caps.ts",
33
+ "validation.ts"
34
+ ],
35
+ "name": "pi-monofold",
36
+ "devDependencies": {
37
+ "typescript": "^6.0.3",
38
+ "typebox": "^1.1.38",
39
+ "@types/node": "^25.9.1",
40
+ "tsx": "^4.20.5",
41
+ "@earendil-works/pi-coding-agent": "^0.75.4",
42
+ "@earendil-works/pi-ai": "^0.75.4"
43
+ },
44
+ "dependencies": {
45
+ "yaml": "^2.8.1"
46
+ },
47
+ "keywords": [
48
+ "pi-package",
49
+ "pi-extension",
50
+ "ai-coding",
51
+ "virtual-monorepo",
52
+ "multi-repo"
53
+ ],
54
+ "repository": {
55
+ "type": "git",
56
+ "url": "git+https://github.com/eiei114/pi-monofold.git"
57
+ },
58
+ "bugs": {
59
+ "url": "https://github.com/eiei114/pi-monofold/issues"
60
+ },
61
+ "homepage": "https://github.com/eiei114/pi-monofold#readme",
62
+ "publishConfig": {
63
+ "access": "public"
64
+ },
65
+ "license": "MIT"
66
+ }