@sherif-fanous/pi-presets-plus 0.1.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/CHANGELOG.md +9 -0
- package/LICENSE +21 -0
- package/README.md +67 -0
- package/package.json +74 -0
- package/src/activation/active-state.ts +25 -0
- package/src/activation/apply.ts +236 -0
- package/src/activation/baseline.ts +32 -0
- package/src/activation/clear.ts +434 -0
- package/src/activation/dirty.ts +69 -0
- package/src/activation/drift-handlers.ts +71 -0
- package/src/activation/drift.ts +77 -0
- package/src/activation/same-set.ts +32 -0
- package/src/activation/state-matches.ts +29 -0
- package/src/activation/thinking.ts +54 -0
- package/src/commands/presets/clear.ts +18 -0
- package/src/commands/presets/index.ts +9 -0
- package/src/commands/presets/notify.ts +22 -0
- package/src/commands/presets/reload.ts +28 -0
- package/src/commands/presets/router.ts +139 -0
- package/src/commands/presets/status.ts +262 -0
- package/src/flag.ts +88 -0
- package/src/hotkey-conflicts.ts +136 -0
- package/src/hotkey-reload-baseline.ts +112 -0
- package/src/hotkeys.ts +104 -0
- package/src/index.ts +171 -0
- package/src/messages.ts +34 -0
- package/src/store/api.ts +262 -0
- package/src/store/load.ts +175 -0
- package/src/store/merge.ts +69 -0
- package/src/store/paths.ts +38 -0
- package/src/store/save.ts +75 -0
- package/src/store/validate.ts +195 -0
- package/src/types.ts +169 -0
- package/src/ui/confirm.ts +126 -0
- package/src/ui/editor.ts +1617 -0
- package/src/ui/filter.ts +79 -0
- package/src/ui/frame.ts +109 -0
- package/src/ui/hotkey-input.ts +242 -0
- package/src/ui/info-dialog.ts +118 -0
- package/src/ui/labels.ts +51 -0
- package/src/ui/picker-state.ts +151 -0
- package/src/ui/picker.ts +982 -0
- package/src/ui/reload-prompt.ts +59 -0
- package/src/ui/status.ts +55 -0
- package/src/ui/widgets.ts +274 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Current-state comparison for active presets.
|
|
3
|
+
*
|
|
4
|
+
* Owns deciding whether pi's current state already matches a preset; it
|
|
5
|
+
* does NOT mutate state or notify users.
|
|
6
|
+
*/
|
|
7
|
+
import type { LoadedPreset } from "../types.js";
|
|
8
|
+
import { detectDriftReasons, snapshotPresetForDrift } from "./drift.js";
|
|
9
|
+
import type {
|
|
10
|
+
ExtensionAPI,
|
|
11
|
+
ExtensionContext,
|
|
12
|
+
} from "@mariozechner/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
/** Minimal context surface needed for comparison. */
|
|
15
|
+
type StateMatchesContext = Pick<ExtensionContext, "model" | "modelRegistry">;
|
|
16
|
+
|
|
17
|
+
/** Minimal pi surface needed for comparison. */
|
|
18
|
+
type StateMatchesPi = Pick<ExtensionAPI, "getActiveTools" | "getThinkingLevel">;
|
|
19
|
+
|
|
20
|
+
/** Return true when current model/thinking/tools equal declared preset state. */
|
|
21
|
+
export function stateMatches(
|
|
22
|
+
preset: LoadedPreset,
|
|
23
|
+
pi: StateMatchesPi,
|
|
24
|
+
ctx: StateMatchesContext,
|
|
25
|
+
): boolean {
|
|
26
|
+
return (
|
|
27
|
+
detectDriftReasons(snapshotPresetForDrift(preset), pi, ctx).length === 0
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thinking-level capability helpers for preset activation.
|
|
3
|
+
*
|
|
4
|
+
* Owns the mapping from a resolved model to the thinking levels a preset
|
|
5
|
+
* may legally apply; it does NOT mutate pi state or surface notifications.
|
|
6
|
+
*/
|
|
7
|
+
import type { Preset, ThinkingLevel } from "../types.js";
|
|
8
|
+
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
9
|
+
|
|
10
|
+
const ALL_THINKING_LEVELS: readonly ThinkingLevel[] = [
|
|
11
|
+
"off",
|
|
12
|
+
"minimal",
|
|
13
|
+
"low",
|
|
14
|
+
"medium",
|
|
15
|
+
"high",
|
|
16
|
+
"xhigh",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
/** Return the level pi will effectively use for the preset/model pair. */
|
|
20
|
+
export function effectiveThinkingLevel(
|
|
21
|
+
preset: Pick<Preset, "thinkingLevel">,
|
|
22
|
+
model: Model<Api> | undefined,
|
|
23
|
+
): ThinkingLevel {
|
|
24
|
+
const declared = preset.thinkingLevel ?? "off";
|
|
25
|
+
|
|
26
|
+
return validThinkingLevels(model).includes(declared) ? declared : "off";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Return the levels meaningful for a model; unknown models are permissive.
|
|
31
|
+
*
|
|
32
|
+
* `model.reasoning === false` is authoritative and allows only `"off"`.
|
|
33
|
+
* Reasoning models mirror pi-ai's supported-level parser: a level is
|
|
34
|
+
* unsupported when the map explicitly stores `null`; missing keys fall through
|
|
35
|
+
* to provider defaults for levels through `"high"`; `"xhigh"` must be
|
|
36
|
+
* explicitly mapped to a non-null value. Optional-chained reads keep older
|
|
37
|
+
* pi-ai bundles that predate `thinkingLevelMap` on the legacy up-to-high
|
|
38
|
+
* behavior.
|
|
39
|
+
*/
|
|
40
|
+
export function validThinkingLevels(
|
|
41
|
+
model: Model<Api> | undefined,
|
|
42
|
+
): ThinkingLevel[] {
|
|
43
|
+
if (!model) return [...ALL_THINKING_LEVELS];
|
|
44
|
+
if (model.reasoning === false) return ["off"];
|
|
45
|
+
|
|
46
|
+
return ALL_THINKING_LEVELS.filter((level) => {
|
|
47
|
+
const mapped = model.thinkingLevelMap?.[level];
|
|
48
|
+
|
|
49
|
+
if (mapped === null) return false;
|
|
50
|
+
if (level === "xhigh") return mapped !== undefined;
|
|
51
|
+
|
|
52
|
+
return true;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/presets clear` command runner.
|
|
3
|
+
*
|
|
4
|
+
* Owns command-bound delegation to the activation clear engine; restore
|
|
5
|
+
* semantics live in `activation/clear.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import { clear } from "../../activation/clear.js";
|
|
8
|
+
import type {
|
|
9
|
+
ExtensionAPI,
|
|
10
|
+
ExtensionCommandContext,
|
|
11
|
+
} from "@mariozechner/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
export async function runClear(
|
|
14
|
+
ctx: ExtensionCommandContext,
|
|
15
|
+
pi: ExtensionAPI,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
await clear(ctx, pi);
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public entry points for the `/presets` command.
|
|
3
|
+
*
|
|
4
|
+
* Owns the barrel that the extension entry point imports from; it does
|
|
5
|
+
* NOT own subcommand routing or implementation details.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { getArgumentCompletions, handlePresetsCommand } from "./router.js";
|
|
9
|
+
export { surfaceWarnings } from "./notify.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared notification helper for `/presets` subcommands.
|
|
3
|
+
*
|
|
4
|
+
* Owns rolling load-time warnings into a single user-visible notification
|
|
5
|
+
* so callers do not flood the UI; it does NOT own loading or validation.
|
|
6
|
+
*/
|
|
7
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Fire a single warning-level notification listing every warning, or
|
|
11
|
+
* no-op when the list is empty.
|
|
12
|
+
*/
|
|
13
|
+
export function surfaceWarnings(
|
|
14
|
+
ctx: Pick<ExtensionContext, "ui">,
|
|
15
|
+
warnings: readonly string[],
|
|
16
|
+
): void {
|
|
17
|
+
if (warnings.length === 0) return;
|
|
18
|
+
ctx.ui.notify(
|
|
19
|
+
`${warnings.length} preset warning${warnings.length === 1 ? "" : "s"}:\n- ${warnings.join("\n- ")}`,
|
|
20
|
+
"warning",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/presets reload` command runner.
|
|
3
|
+
*
|
|
4
|
+
* Owns re-reading both scope files on demand and reporting the result to
|
|
5
|
+
* the user as a single notification; it does NOT own the underlying
|
|
6
|
+
* storage layer or activation state.
|
|
7
|
+
*/
|
|
8
|
+
import { loadAll } from "../../store/api.js";
|
|
9
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Run the `reload` subcommand against a live `ExtensionContext`.
|
|
13
|
+
*/
|
|
14
|
+
export async function runReload(ctx: ExtensionContext): Promise<void> {
|
|
15
|
+
const { presets, warnings } = await loadAll(ctx);
|
|
16
|
+
const summary = `Reloaded ${presets.length} preset${presets.length === 1 ? "" : "s"}.`;
|
|
17
|
+
|
|
18
|
+
if (warnings.length === 0) {
|
|
19
|
+
ctx.ui.notify(summary, "info");
|
|
20
|
+
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
ctx.ui.notify(
|
|
25
|
+
`${summary}\n${warnings.length} warning${warnings.length === 1 ? "" : "s"}:\n- ${warnings.join("\n- ")}`,
|
|
26
|
+
"warning",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/presets` subcommand router.
|
|
3
|
+
*
|
|
4
|
+
* Owns command token dispatch and autocomplete for the `/presets` command;
|
|
5
|
+
* storage, activation, picker, and clear semantics live in their dedicated
|
|
6
|
+
* modules.
|
|
7
|
+
*/
|
|
8
|
+
import { apply } from "../../activation/apply.js";
|
|
9
|
+
import { openPicker } from "../../ui/picker.js";
|
|
10
|
+
import { runClear } from "./clear.js";
|
|
11
|
+
import { runReload } from "./reload.js";
|
|
12
|
+
import { runStatus } from "./status.js";
|
|
13
|
+
import type {
|
|
14
|
+
ExtensionAPI,
|
|
15
|
+
ExtensionCommandContext,
|
|
16
|
+
} from "@mariozechner/pi-coding-agent";
|
|
17
|
+
|
|
18
|
+
interface Subcommand {
|
|
19
|
+
readonly value: string;
|
|
20
|
+
readonly label: string;
|
|
21
|
+
run(
|
|
22
|
+
ctx: ExtensionCommandContext,
|
|
23
|
+
args: readonly string[],
|
|
24
|
+
pi?: ExtensionAPI,
|
|
25
|
+
): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SUBCOMMANDS: readonly Subcommand[] = [
|
|
29
|
+
{
|
|
30
|
+
value: "reload",
|
|
31
|
+
label: "reload: re-read both scope files",
|
|
32
|
+
run: runReloadWrapper,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: "clear",
|
|
36
|
+
label: "clear: clear the active preset",
|
|
37
|
+
run: runClearWrapper,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
value: "status",
|
|
41
|
+
label: "status: show active preset details",
|
|
42
|
+
run: runStatusWrapper,
|
|
43
|
+
},
|
|
44
|
+
] as const;
|
|
45
|
+
|
|
46
|
+
export function getArgumentCompletions(
|
|
47
|
+
prefix: string,
|
|
48
|
+
): { value: string; label: string }[] {
|
|
49
|
+
const trimmedPrefix = prefix.trimStart();
|
|
50
|
+
|
|
51
|
+
if (trimmedPrefix.includes(" ")) return [];
|
|
52
|
+
|
|
53
|
+
return SUBCOMMANDS.filter((subcommand) =>
|
|
54
|
+
subcommand.value.startsWith(trimmedPrefix),
|
|
55
|
+
).map(({ value, label }) => ({ value, label }));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function handlePresetsCommand(
|
|
59
|
+
args: string,
|
|
60
|
+
ctx: ExtensionCommandContext,
|
|
61
|
+
pi?: ExtensionAPI,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
const trimmedArgs = args.trim();
|
|
64
|
+
|
|
65
|
+
if (trimmedArgs.length === 0) {
|
|
66
|
+
await runPicker(ctx, pi);
|
|
67
|
+
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const tokens = trimmedArgs.split(/\s+/);
|
|
72
|
+
const subCommand = tokens[0] ?? "";
|
|
73
|
+
|
|
74
|
+
if (subCommand === "list") {
|
|
75
|
+
ctx.ui.notify(
|
|
76
|
+
'"list" is not a supported /presets subcommand. Run /presets to open the picker.',
|
|
77
|
+
"warning",
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const target = SUBCOMMANDS.find(
|
|
84
|
+
(subcommand) => subcommand.value === subCommand,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (target) {
|
|
88
|
+
await target.run(ctx, tokens.slice(1), pi);
|
|
89
|
+
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ctx.ui.notify(
|
|
94
|
+
`unknown subcommand "${subCommand ?? ""}". try /presets, /presets reload, /presets clear, or /presets status.`,
|
|
95
|
+
"warning",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function runClearWrapper(
|
|
100
|
+
ctx: ExtensionCommandContext,
|
|
101
|
+
_args: readonly string[],
|
|
102
|
+
pi?: ExtensionAPI,
|
|
103
|
+
): Promise<void> {
|
|
104
|
+
if (!pi) return;
|
|
105
|
+
await runClear(ctx, pi);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function runPicker(
|
|
109
|
+
ctx: ExtensionCommandContext,
|
|
110
|
+
pi?: ExtensionAPI,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
if (!pi) {
|
|
113
|
+
ctx.ui.notify(
|
|
114
|
+
"Preset picker is only available in interactive mode.",
|
|
115
|
+
"warning",
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await openPicker(ctx, {
|
|
122
|
+
inheritedTools: pi.getActiveTools(),
|
|
123
|
+
onActivate: (preset) => apply(preset, ctx, pi),
|
|
124
|
+
pi,
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function runReloadWrapper(ctx: ExtensionCommandContext): Promise<void> {
|
|
129
|
+
await runReload(ctx);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function runStatusWrapper(
|
|
133
|
+
ctx: ExtensionCommandContext,
|
|
134
|
+
_args: readonly string[],
|
|
135
|
+
pi?: ExtensionAPI,
|
|
136
|
+
): Promise<void> {
|
|
137
|
+
if (!pi) return;
|
|
138
|
+
await runStatus(ctx, pi);
|
|
139
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/presets status` textual diagnostic.
|
|
3
|
+
*
|
|
4
|
+
* Owns formatting the active preset and its baseline-overlay state into a
|
|
5
|
+
* user-facing report; it does NOT update the footer indicator or mutate
|
|
6
|
+
* the active attachment.
|
|
7
|
+
*/
|
|
8
|
+
import { getActive } from "../../activation/active-state.js";
|
|
9
|
+
import { loadAll } from "../../store/api.js";
|
|
10
|
+
import type { LoadedPreset } from "../../types.js";
|
|
11
|
+
import {
|
|
12
|
+
BASELINE_MODEL_LABEL,
|
|
13
|
+
BASELINE_THINKING_LABEL,
|
|
14
|
+
BASELINE_TOOLS_LABEL,
|
|
15
|
+
CURRENT_MODEL_LABEL,
|
|
16
|
+
CURRENT_THINKING_LABEL,
|
|
17
|
+
CURRENT_TOOLS_LABEL,
|
|
18
|
+
PRESET_LABEL,
|
|
19
|
+
PRESET_MODEL_LABEL,
|
|
20
|
+
PRESET_THINKING_LABEL,
|
|
21
|
+
PRESET_TOOLS_LABEL,
|
|
22
|
+
RESTORE_LABEL,
|
|
23
|
+
SCOPE_LABEL,
|
|
24
|
+
STATUS_DIALOG_TITLE,
|
|
25
|
+
} from "../../ui/labels.js";
|
|
26
|
+
import { surfaceWarnings } from "./notify.js";
|
|
27
|
+
import type {
|
|
28
|
+
ExtensionAPI,
|
|
29
|
+
ExtensionCommandContext,
|
|
30
|
+
Theme,
|
|
31
|
+
} from "@mariozechner/pi-coding-agent";
|
|
32
|
+
|
|
33
|
+
export interface StatusBodyResult {
|
|
34
|
+
readonly body: string;
|
|
35
|
+
readonly severity: "info" | "warning";
|
|
36
|
+
readonly warnings: readonly string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface Styler {
|
|
40
|
+
bold(text: string): string;
|
|
41
|
+
fg(color: string, text: string): string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const IDENTITY_STYLER: Styler = {
|
|
45
|
+
bold: (text) => text,
|
|
46
|
+
fg: (_color, text) => text,
|
|
47
|
+
};
|
|
48
|
+
const STATUS_LABELS = [
|
|
49
|
+
`${PRESET_LABEL}:`,
|
|
50
|
+
`${SCOPE_LABEL}:`,
|
|
51
|
+
`${RESTORE_LABEL}:`,
|
|
52
|
+
`${BASELINE_MODEL_LABEL}:`,
|
|
53
|
+
`${BASELINE_THINKING_LABEL}:`,
|
|
54
|
+
`${BASELINE_TOOLS_LABEL}:`,
|
|
55
|
+
`${PRESET_MODEL_LABEL}:`,
|
|
56
|
+
`${PRESET_THINKING_LABEL}:`,
|
|
57
|
+
`${PRESET_TOOLS_LABEL}:`,
|
|
58
|
+
`${CURRENT_MODEL_LABEL}:`,
|
|
59
|
+
`${CURRENT_THINKING_LABEL}:`,
|
|
60
|
+
`${CURRENT_TOOLS_LABEL}:`,
|
|
61
|
+
] as const;
|
|
62
|
+
const STATUS_LABEL_WIDTH = Math.max(
|
|
63
|
+
...STATUS_LABELS.map((label) => label.length),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
export function formatStatus(
|
|
67
|
+
active: ReturnType<typeof getActive>,
|
|
68
|
+
_preset: LoadedPreset,
|
|
69
|
+
ctx: Pick<ExtensionCommandContext, "model">,
|
|
70
|
+
pi: Pick<ExtensionAPI, "getActiveTools" | "getThinkingLevel">,
|
|
71
|
+
styler: Pick<Theme, "bold" | "fg"> = IDENTITY_STYLER,
|
|
72
|
+
): string {
|
|
73
|
+
if (!active) return "No preset is active.";
|
|
74
|
+
|
|
75
|
+
const currentModel = ctx.model
|
|
76
|
+
? { provider: ctx.model.provider, id: ctx.model.id }
|
|
77
|
+
: null;
|
|
78
|
+
const currentTools = pi.getActiveTools();
|
|
79
|
+
|
|
80
|
+
if (active.restore.kind === "unknown") {
|
|
81
|
+
return [
|
|
82
|
+
styler.bold(styler.fg("accent", STATUS_DIALOG_TITLE)),
|
|
83
|
+
row(`${PRESET_LABEL}:`, active.name, styler),
|
|
84
|
+
row(`${SCOPE_LABEL}:`, active.scope, styler),
|
|
85
|
+
row(
|
|
86
|
+
`${RESTORE_LABEL}:`,
|
|
87
|
+
"No saved baseline. Clear will only turn the preset off.",
|
|
88
|
+
styler,
|
|
89
|
+
),
|
|
90
|
+
row(`${CURRENT_MODEL_LABEL}:`, formatModel(currentModel), styler),
|
|
91
|
+
row(`${CURRENT_THINKING_LABEL}:`, pi.getThinkingLevel(), styler),
|
|
92
|
+
row(`${CURRENT_TOOLS_LABEL}:`, formatTools(currentTools), styler),
|
|
93
|
+
].join("\n");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { baseline, lastApplied, owned } = active.restore;
|
|
97
|
+
const modelClass = classifyField(
|
|
98
|
+
currentModel,
|
|
99
|
+
baseline.model,
|
|
100
|
+
lastApplied.model,
|
|
101
|
+
);
|
|
102
|
+
const thinkingClass = classifyField(
|
|
103
|
+
pi.getThinkingLevel(),
|
|
104
|
+
baseline.thinkingLevel,
|
|
105
|
+
lastApplied.thinkingLevel,
|
|
106
|
+
);
|
|
107
|
+
const toolsClass = owned.tools
|
|
108
|
+
? classifyField(currentTools, baseline.tools, lastApplied.tools ?? [])
|
|
109
|
+
: "Not managed by active preset";
|
|
110
|
+
|
|
111
|
+
return [
|
|
112
|
+
styler.bold(styler.fg("accent", STATUS_DIALOG_TITLE)),
|
|
113
|
+
row(`${PRESET_LABEL}:`, active.name, styler),
|
|
114
|
+
row(`${SCOPE_LABEL}:`, active.scope, styler),
|
|
115
|
+
row(`${BASELINE_MODEL_LABEL}:`, formatModel(baseline.model), styler),
|
|
116
|
+
row(`${BASELINE_THINKING_LABEL}:`, baseline.thinkingLevel, styler),
|
|
117
|
+
row(`${BASELINE_TOOLS_LABEL}:`, formatTools(baseline.tools), styler),
|
|
118
|
+
row(`${PRESET_MODEL_LABEL}:`, formatModel(lastApplied.model), styler),
|
|
119
|
+
row(`${PRESET_THINKING_LABEL}:`, lastApplied.thinkingLevel, styler),
|
|
120
|
+
row(
|
|
121
|
+
`${PRESET_TOOLS_LABEL}:`,
|
|
122
|
+
lastApplied.tools ? formatTools(lastApplied.tools) : "none",
|
|
123
|
+
styler,
|
|
124
|
+
),
|
|
125
|
+
row(
|
|
126
|
+
`${CURRENT_MODEL_LABEL}:`,
|
|
127
|
+
`${formatModel(currentModel)} (${modelClass})`,
|
|
128
|
+
styler,
|
|
129
|
+
),
|
|
130
|
+
row(
|
|
131
|
+
`${CURRENT_THINKING_LABEL}:`,
|
|
132
|
+
`${pi.getThinkingLevel()} (${thinkingClass})`,
|
|
133
|
+
styler,
|
|
134
|
+
),
|
|
135
|
+
row(
|
|
136
|
+
`${CURRENT_TOOLS_LABEL}:`,
|
|
137
|
+
`${formatTools(currentTools)} (${toolsClass})`,
|
|
138
|
+
styler,
|
|
139
|
+
),
|
|
140
|
+
].join("\n");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function formatStatusBody(
|
|
144
|
+
ctx: ExtensionCommandContext,
|
|
145
|
+
pi: ExtensionAPI,
|
|
146
|
+
): Promise<StatusBodyResult> {
|
|
147
|
+
const active = getActive();
|
|
148
|
+
|
|
149
|
+
if (!active) {
|
|
150
|
+
return { body: "No preset is active.", severity: "info", warnings: [] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { presets, warnings } = await loadAll(ctx);
|
|
154
|
+
const preset = presets.find(
|
|
155
|
+
(candidate) =>
|
|
156
|
+
candidate.name === active.name && candidate.scope === active.scope,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
if (!preset) {
|
|
160
|
+
return {
|
|
161
|
+
body: `Active preset "${active.name}" is no longer loaded.`,
|
|
162
|
+
severity: "warning",
|
|
163
|
+
warnings,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
body: formatStatus(active, preset, ctx, pi, ctx.ui.theme),
|
|
169
|
+
severity: "info",
|
|
170
|
+
warnings,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export async function runStatus(
|
|
175
|
+
ctx: ExtensionCommandContext,
|
|
176
|
+
pi: ExtensionAPI,
|
|
177
|
+
): Promise<void> {
|
|
178
|
+
const result = await formatStatusBody(ctx, pi);
|
|
179
|
+
|
|
180
|
+
surfaceWarnings(ctx, result.warnings);
|
|
181
|
+
ctx.ui.notify(result.body, result.severity);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Compare a current value against the baseline and last-applied snapshots
|
|
186
|
+
* and return a plain-English label.
|
|
187
|
+
*
|
|
188
|
+
* Vocabulary parallels the per-row annotations in `renderClearSummary` so
|
|
189
|
+
* users see the same phrasing across `/presets status` and `/presets clear`.
|
|
190
|
+
* `compare` is `===` for primitives and bag-equality for tool arrays;
|
|
191
|
+
* model objects compare provider+id.
|
|
192
|
+
*/
|
|
193
|
+
function classifyField(
|
|
194
|
+
current: { provider: string; id: string } | null | readonly string[] | string,
|
|
195
|
+
baseline:
|
|
196
|
+
| { provider: string; id: string }
|
|
197
|
+
| null
|
|
198
|
+
| readonly string[]
|
|
199
|
+
| string,
|
|
200
|
+
lastApplied: { provider: string; id: string } | readonly string[] | string,
|
|
201
|
+
):
|
|
202
|
+
| "Already at baseline"
|
|
203
|
+
| "Left as-is — you changed it after activation"
|
|
204
|
+
| "Managed by active preset" {
|
|
205
|
+
if (sameComparable(current, baseline)) return "Already at baseline";
|
|
206
|
+
if (sameComparable(current, lastApplied)) return "Managed by active preset";
|
|
207
|
+
|
|
208
|
+
return "Left as-is — you changed it after activation";
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function formatModel(model: { provider: string; id: string } | null): string {
|
|
212
|
+
return model ? `${model.provider}/${model.id}` : "none";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function formatTools(tools: readonly string[]): string {
|
|
216
|
+
return tools.length > 0 ? tools.join(", ") : "none";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function row(
|
|
220
|
+
label: (typeof STATUS_LABELS)[number],
|
|
221
|
+
value: string,
|
|
222
|
+
styler: Pick<Theme, "fg">,
|
|
223
|
+
): string {
|
|
224
|
+
const padding = " ".repeat(STATUS_LABEL_WIDTH - label.length);
|
|
225
|
+
|
|
226
|
+
return ` ${styler.fg("muted", label)}${padding} ${value}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function sameComparable(
|
|
230
|
+
left: { provider: string; id: string } | null | readonly string[] | string,
|
|
231
|
+
right: { provider: string; id: string } | null | readonly string[] | string,
|
|
232
|
+
): boolean {
|
|
233
|
+
const leftIsArray = Array.isArray(left);
|
|
234
|
+
const rightIsArray = Array.isArray(right);
|
|
235
|
+
|
|
236
|
+
if (leftIsArray || rightIsArray) {
|
|
237
|
+
if (!leftIsArray || !rightIsArray) return false;
|
|
238
|
+
|
|
239
|
+
const leftArr = left as readonly string[];
|
|
240
|
+
const rightArr = right as readonly string[];
|
|
241
|
+
|
|
242
|
+
if (leftArr.length !== rightArr.length) return false;
|
|
243
|
+
|
|
244
|
+
const rightSet = new Set(rightArr);
|
|
245
|
+
|
|
246
|
+
return leftArr.every((value) => rightSet.has(value));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (typeof left === "string" || typeof right === "string") {
|
|
250
|
+
return (
|
|
251
|
+
typeof left === "string" && typeof right === "string" && left === right
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const leftModel = left as { provider: string; id: string } | null;
|
|
256
|
+
const rightModel = right as { provider: string; id: string } | null;
|
|
257
|
+
|
|
258
|
+
return (
|
|
259
|
+
leftModel?.provider === rightModel?.provider &&
|
|
260
|
+
leftModel?.id === rightModel?.id
|
|
261
|
+
);
|
|
262
|
+
}
|
package/src/flag.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup preset flag registration and handling.
|
|
3
|
+
*
|
|
4
|
+
* Owns the `--preset` CLI flag entry point and startup lookup messages. It
|
|
5
|
+
* does NOT own session restore, preset storage, or the apply implementation.
|
|
6
|
+
*/
|
|
7
|
+
import { apply } from "./activation/apply.js";
|
|
8
|
+
import type { LoadedPreset } from "./types.js";
|
|
9
|
+
import type {
|
|
10
|
+
ExtensionAPI,
|
|
11
|
+
ExtensionContext,
|
|
12
|
+
} from "@mariozechner/pi-coding-agent";
|
|
13
|
+
|
|
14
|
+
const PRESET_FLAG = "preset";
|
|
15
|
+
|
|
16
|
+
export async function applyPresetFlag(
|
|
17
|
+
pi: ExtensionAPI,
|
|
18
|
+
ctx: ExtensionContext,
|
|
19
|
+
presets: readonly LoadedPreset[],
|
|
20
|
+
): Promise<void> {
|
|
21
|
+
const value = pi.getFlag(PRESET_FLAG);
|
|
22
|
+
|
|
23
|
+
if (typeof value !== "string") return;
|
|
24
|
+
|
|
25
|
+
const name = value.trim();
|
|
26
|
+
|
|
27
|
+
if (name.length === 0) return;
|
|
28
|
+
|
|
29
|
+
const preset = findPresetForFlag(presets, name);
|
|
30
|
+
|
|
31
|
+
if (!preset) {
|
|
32
|
+
ctx.ui.notify(
|
|
33
|
+
`--preset: Unknown preset "${name}". Available: ${formatAvailableNames(presets)}.`,
|
|
34
|
+
"warning",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const result = await apply(preset, ctx, pi);
|
|
41
|
+
|
|
42
|
+
if (!result.ok) ctx.ui.notify(result.reason, "error");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function registerPresetFlag(
|
|
46
|
+
pi: Pick<ExtensionAPI, "registerFlag">,
|
|
47
|
+
): void {
|
|
48
|
+
pi.registerFlag(PRESET_FLAG, {
|
|
49
|
+
description: "Activate the named pi-presets-plus preset on session start.",
|
|
50
|
+
type: "string",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findPresetForFlag(
|
|
55
|
+
presets: readonly LoadedPreset[],
|
|
56
|
+
name: string,
|
|
57
|
+
): LoadedPreset | undefined {
|
|
58
|
+
return (
|
|
59
|
+
presets.find(
|
|
60
|
+
(preset) =>
|
|
61
|
+
preset.name === name && preset.scope === "project" && !preset.shadowed,
|
|
62
|
+
) ??
|
|
63
|
+
presets.find(
|
|
64
|
+
(preset) =>
|
|
65
|
+
preset.name === name && preset.scope === "user" && !preset.shadowed,
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatAvailableNames(presets: readonly LoadedPreset[]): string {
|
|
71
|
+
const byName = new Map<string, LoadedPreset>();
|
|
72
|
+
|
|
73
|
+
for (const preset of presets) {
|
|
74
|
+
const existing = byName.get(preset.name);
|
|
75
|
+
|
|
76
|
+
if (!existing || existing.shadowed) byName.set(preset.name, preset);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (byName.size === 0) return "none";
|
|
80
|
+
|
|
81
|
+
return [...byName.values()]
|
|
82
|
+
.map((preset) =>
|
|
83
|
+
preset.unavailable
|
|
84
|
+
? `${preset.name} (Unavailable: ${preset.unavailable})`
|
|
85
|
+
: preset.name,
|
|
86
|
+
)
|
|
87
|
+
.join(", ");
|
|
88
|
+
}
|