@sherif-fanous/pi-presets-plus 0.1.0 → 0.1.2
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 +15 -0
- package/package.json +1 -1
- package/src/activation/apply.ts +22 -58
- package/src/activation/clear.ts +20 -175
- package/src/activation/drift-handlers.ts +10 -8
- package/src/activation/session.ts +243 -0
- package/src/commands/presets/clear.ts +3 -1
- package/src/commands/presets/router.ts +26 -10
- package/src/commands/presets/status.ts +8 -8
- package/src/flag.ts +7 -1
- package/src/hotkey-registry.ts +317 -0
- package/src/index.ts +33 -98
- package/src/preset-identity.ts +34 -0
- package/src/store/api.ts +6 -6
- package/src/ui/clear-summary.ts +167 -0
- package/src/ui/editor.ts +21 -21
- package/src/ui/picker.ts +33 -14
- package/src/ui/status.ts +17 -40
- package/src/activation/active-state.ts +0 -25
- package/src/activation/dirty.ts +0 -69
- package/src/hotkey-conflicts.ts +0 -136
- package/src/hotkey-reload-baseline.ts +0 -112
- package/src/hotkeys.ts +0 -104
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
This changelog follows [Common Changelog](https://common-changelog.org/).
|
|
4
4
|
|
|
5
|
+
## [0.1.2] - 2026-05-09
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- Pressing `c` (clear) inside the preset picker with no preset active no longer opens an empty confirm-then-nothing dialog. The picker now shows an info-dialog stating "No preset is active." and returns to the picker.
|
|
10
|
+
- The session-start notification for a preset that shadows a Pi built-in now uses warning severity to match the visual treatment of preset-vs-preset hotkey conflicts. Both collision-style notifications render consistently.
|
|
11
|
+
|
|
12
|
+
## [0.1.1] - 2026-05-09
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Refactored the extension's internal architecture so the active-preset state, the runtime hotkey bindings, and the clear-summary renderer each live in a single dedicated module. No user-visible behavior change; the picker, editor, `/presets` subcommands, `--preset` flag, hotkeys, drift detection, and session restore all behave identically.
|
|
17
|
+
|
|
5
18
|
## [0.1.0] - 2026-05-09
|
|
6
19
|
|
|
7
20
|
_Initial release._
|
|
8
21
|
|
|
22
|
+
[0.1.2]: https://github.com/sherif-fanous/pi-presets-plus/releases/tag/v0.1.2
|
|
23
|
+
[0.1.1]: https://github.com/sherif-fanous/pi-presets-plus/releases/tag/v0.1.1
|
|
9
24
|
[0.1.0]: https://github.com/sherif-fanous/pi-presets-plus/releases/tag/v0.1.0
|
package/package.json
CHANGED
package/src/activation/apply.ts
CHANGED
|
@@ -7,12 +7,10 @@
|
|
|
7
7
|
* picker UI.
|
|
8
8
|
*/
|
|
9
9
|
import { ACTIVATED_MESSAGE_TYPE } from "../messages.js";
|
|
10
|
+
import { samePresetIdentity } from "../preset-identity.js";
|
|
10
11
|
import type { LoadedPreset } from "../types.js";
|
|
11
|
-
import { updateStatus } from "../ui/status.js";
|
|
12
|
-
import { getActive, setActive } from "./active-state.js";
|
|
13
12
|
import { captureBaseline } from "./baseline.js";
|
|
14
|
-
import {
|
|
15
|
-
import { snapshotPresetForDrift } from "./drift.js";
|
|
13
|
+
import type { ActivePresetSession } from "./session.js";
|
|
16
14
|
import { stateMatches } from "./state-matches.js";
|
|
17
15
|
import { effectiveThinkingLevel } from "./thinking.js";
|
|
18
16
|
import type {
|
|
@@ -20,14 +18,6 @@ import type {
|
|
|
20
18
|
ExtensionContext,
|
|
21
19
|
} from "@mariozechner/pi-coding-agent";
|
|
22
20
|
|
|
23
|
-
/**
|
|
24
|
-
* Counter (not boolean) so nested `withSelfTriggeredModelSet` calls cannot
|
|
25
|
-
* silently drop the guard when the inner call's `finally` runs while the
|
|
26
|
-
* outer call is still in flight. Today there is only one caller, but the
|
|
27
|
-
* counter shape costs nothing and removes a footgun for future callers.
|
|
28
|
-
*/
|
|
29
|
-
let selfTriggeredModelSetDepth = 0;
|
|
30
|
-
|
|
31
21
|
/**
|
|
32
22
|
* In-memory result from applying a preset.
|
|
33
23
|
*
|
|
@@ -55,6 +45,7 @@ export async function apply(
|
|
|
55
45
|
preset: LoadedPreset,
|
|
56
46
|
ctx: ExtensionContext,
|
|
57
47
|
pi: ExtensionAPI,
|
|
48
|
+
session: ActivePresetSession,
|
|
58
49
|
): Promise<ApplyResult> {
|
|
59
50
|
if (preset.unavailable) {
|
|
60
51
|
const kind = preset.unavailable;
|
|
@@ -62,15 +53,15 @@ export async function apply(
|
|
|
62
53
|
return { ok: false, kind, reason: failureReason(kind, preset) };
|
|
63
54
|
}
|
|
64
55
|
|
|
65
|
-
const current =
|
|
56
|
+
const current = session.current();
|
|
66
57
|
|
|
67
58
|
if (
|
|
68
|
-
current
|
|
69
|
-
current
|
|
59
|
+
current &&
|
|
60
|
+
samePresetIdentity(current, preset) &&
|
|
70
61
|
current.restore.kind === "baseline" &&
|
|
71
62
|
stateMatches(preset, pi, ctx)
|
|
72
63
|
) {
|
|
73
|
-
if (current.dirty)
|
|
64
|
+
if (current.dirty) session.markClean(ctx);
|
|
74
65
|
|
|
75
66
|
return { ok: true };
|
|
76
67
|
}
|
|
@@ -92,7 +83,7 @@ export async function apply(
|
|
|
92
83
|
const previousAppliedTools = previousBaseline?.lastApplied.tools;
|
|
93
84
|
const previousOwnedTools = previousBaseline?.owned.tools ?? false;
|
|
94
85
|
|
|
95
|
-
if (!(await setModelGuarded(pi, model))) {
|
|
86
|
+
if (!(await setModelGuarded(pi, model, session))) {
|
|
96
87
|
return {
|
|
97
88
|
ok: false,
|
|
98
89
|
kind: "key-revoked",
|
|
@@ -133,28 +124,27 @@ export async function apply(
|
|
|
133
124
|
ownedTools = true;
|
|
134
125
|
}
|
|
135
126
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
127
|
+
// session.start commits the new active state (and refreshes the status
|
|
128
|
+
// badge as part of that commit) BEFORE the customType message is
|
|
129
|
+
// emitted, so observers seeing the "Preset … applied" message can
|
|
130
|
+
// already query the updated state. The reverse order — message first,
|
|
131
|
+
// status second — would briefly publish an event whose corresponding
|
|
132
|
+
// state is not yet visible.
|
|
133
|
+
session.start(
|
|
134
|
+
{
|
|
141
135
|
applyCount,
|
|
142
136
|
baseline,
|
|
143
|
-
kind: "baseline",
|
|
144
137
|
lastApplied: {
|
|
145
138
|
...(appliedTools !== undefined ? { tools: appliedTools } : {}),
|
|
146
139
|
model: { id: preset.model, provider: preset.provider },
|
|
147
140
|
thinkingLevel: effective,
|
|
148
141
|
},
|
|
149
142
|
owned: { model: true, thinkingLevel: true, tools: ownedTools },
|
|
143
|
+
preset,
|
|
150
144
|
},
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
pi.appendEntry("presets-plus:active", {
|
|
155
|
-
name: preset.name,
|
|
156
|
-
scope: preset.scope,
|
|
157
|
-
});
|
|
145
|
+
ctx,
|
|
146
|
+
pi,
|
|
147
|
+
);
|
|
158
148
|
|
|
159
149
|
pi.sendMessage({
|
|
160
150
|
customType: ACTIVATED_MESSAGE_TYPE,
|
|
@@ -167,36 +157,9 @@ export async function apply(
|
|
|
167
157
|
},
|
|
168
158
|
});
|
|
169
159
|
|
|
170
|
-
updateStatus(ctx, getActive(), (name, scope) =>
|
|
171
|
-
name === preset.name && scope === preset.scope ? preset : undefined,
|
|
172
|
-
);
|
|
173
|
-
|
|
174
160
|
return { ok: true };
|
|
175
161
|
}
|
|
176
162
|
|
|
177
|
-
export function isSelfTriggeredModelSet(): boolean {
|
|
178
|
-
return selfTriggeredModelSetDepth > 0;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Run `fn` with the self-call counter raised so a `model_select` event
|
|
183
|
-
* triggered by `pi.setModel` inside `fn` is recognized as our own write
|
|
184
|
-
* and ignored by the drift-detection handler. The counter shape (vs. a
|
|
185
|
-
* boolean) keeps nested or concurrent calls from accidentally lowering the
|
|
186
|
-
* guard while an outer call is still in flight.
|
|
187
|
-
*/
|
|
188
|
-
export async function withSelfTriggeredModelSet<T>(
|
|
189
|
-
fn: () => Promise<T>,
|
|
190
|
-
): Promise<T> {
|
|
191
|
-
selfTriggeredModelSetDepth++;
|
|
192
|
-
|
|
193
|
-
try {
|
|
194
|
-
return await fn();
|
|
195
|
-
} finally {
|
|
196
|
-
selfTriggeredModelSetDepth--;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
163
|
function failureReason(
|
|
201
164
|
kind: Exclude<ApplyResult, { ok: true }>["kind"],
|
|
202
165
|
preset: Pick<LoadedPreset, "model" | "name" | "provider">,
|
|
@@ -231,6 +194,7 @@ function filterValidTools(
|
|
|
231
194
|
async function setModelGuarded(
|
|
232
195
|
pi: Pick<ExtensionAPI, "setModel">,
|
|
233
196
|
model: NonNullable<ReturnType<ExtensionContext["modelRegistry"]["find"]>>,
|
|
197
|
+
session: ActivePresetSession,
|
|
234
198
|
): Promise<boolean> {
|
|
235
|
-
return withSelfTriggeredModelSet(() => pi.setModel(model));
|
|
199
|
+
return session.withSelfTriggeredModelSet(() => pi.setModel(model));
|
|
236
200
|
}
|
package/src/activation/clear.ts
CHANGED
|
@@ -2,24 +2,20 @@
|
|
|
2
2
|
* Active-preset clear flow.
|
|
3
3
|
*
|
|
4
4
|
* Owns restoring pi state from the baseline overlay (with user-override
|
|
5
|
-
* protection) and
|
|
6
|
-
*
|
|
5
|
+
* protection) and deciding clear outcomes; it does NOT own apply, picker UI,
|
|
6
|
+
* or pure clear-summary rendering.
|
|
7
7
|
*/
|
|
8
8
|
import type { ActivePresetState, ThinkingLevel } from "../types.js";
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
} from "../ui/labels.js";
|
|
15
|
-
import { updateStatus } from "../ui/status.js";
|
|
16
|
-
import { clearActive, getActive } from "./active-state.js";
|
|
17
|
-
import { withSelfTriggeredModelSet } from "./apply.js";
|
|
10
|
+
formatModel,
|
|
11
|
+
formatTools,
|
|
12
|
+
renderClearSummary,
|
|
13
|
+
} from "../ui/clear-summary.js";
|
|
18
14
|
import { sameSet } from "./same-set.js";
|
|
15
|
+
import type { ActivePresetSession } from "./session.js";
|
|
19
16
|
import type {
|
|
20
17
|
ExtensionAPI,
|
|
21
18
|
ExtensionCommandContext,
|
|
22
|
-
Theme,
|
|
23
19
|
} from "@mariozechner/pi-coding-agent";
|
|
24
20
|
|
|
25
21
|
export interface ClearDecision {
|
|
@@ -45,6 +41,11 @@ export interface ClearPart {
|
|
|
45
41
|
readonly value: string;
|
|
46
42
|
}
|
|
47
43
|
|
|
44
|
+
export interface ClearResult {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly parts: readonly ClearPart[];
|
|
47
|
+
}
|
|
48
|
+
|
|
48
49
|
export interface ClearSnapshot {
|
|
49
50
|
readonly active: ActivePresetState;
|
|
50
51
|
readonly allTools: readonly string[];
|
|
@@ -71,73 +72,12 @@ export type ClearAction =
|
|
|
71
72
|
|
|
72
73
|
export type ClearField = "model" | "thinking" | "tools";
|
|
73
74
|
|
|
74
|
-
interface Styler {
|
|
75
|
-
bold(text: string): string;
|
|
76
|
-
fg(color: Parameters<Theme["fg"]>[0], text: string): string;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const FIELD_LABELS: Record<ClearField, string> = {
|
|
80
|
-
model: MODEL_LABEL,
|
|
81
|
-
thinking: THINKING_LABEL,
|
|
82
|
-
tools: TOOLS_LABEL,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
const IDENTITY_STYLER: Styler = {
|
|
86
|
-
bold: (text) => text,
|
|
87
|
-
fg: (_color, text) => text,
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export interface ClearResult {
|
|
91
|
-
readonly name: string;
|
|
92
|
-
readonly parts: readonly ClearPart[];
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Choose the plain-English lead sentence that sits under the title.
|
|
97
|
-
*
|
|
98
|
-
* The sentence describes the overall disposition so the per-row values
|
|
99
|
-
* underneath can stay short. Decision priority (most specific first):
|
|
100
|
-
*
|
|
101
|
-
* 1. Every field is `unknown` (priorUnknown branch) — no baseline saved.
|
|
102
|
-
* 2. Any field failed to restore — surface the problem in the lead.
|
|
103
|
-
* 3. Every field already matched baseline — nothing was actually written.
|
|
104
|
-
* 4. Every field is restore-like (restored / restored-partial /
|
|
105
|
-
* already-baseline) — the happy path; mention unavailable tools if any.
|
|
106
|
-
* 5. Every field was kept (user-override / not-owned / baseline-null) —
|
|
107
|
-
* preset turned off but no baseline values were applicable.
|
|
108
|
-
* 6. Otherwise it's a mixed result.
|
|
109
|
-
*/
|
|
110
|
-
export function chooseClearLead(parts: readonly ClearPart[]): string {
|
|
111
|
-
if (parts.every((part) => part.action === "unknown")) {
|
|
112
|
-
return "No saved baseline. Current settings were left as-is.";
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (parts.some((part) => part.action === "restore-failed")) {
|
|
116
|
-
return "Tried to restore your previous settings but ran into a problem.";
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (parts.every((part) => part.action === "already-baseline")) {
|
|
120
|
-
return "Your settings already matched the saved baseline.";
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (parts.every((part) => isRestoreLike(part.action))) {
|
|
124
|
-
return parts.some((part) => part.action === "restored-partial")
|
|
125
|
-
? "Restored your previous settings. Some tools are no longer available."
|
|
126
|
-
: "Restored your previous settings.";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (parts.every((part) => isKeptLike(part.action))) {
|
|
130
|
-
return "Kept all your manual changes. Nothing to restore.";
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return "Restored some settings. Kept your manual changes for others.";
|
|
134
|
-
}
|
|
135
|
-
|
|
136
75
|
export async function clear(
|
|
137
76
|
ctx: ExtensionCommandContext,
|
|
138
77
|
pi: ExtensionAPI,
|
|
78
|
+
session: ActivePresetSession,
|
|
139
79
|
): Promise<void> {
|
|
140
|
-
const result = await clearReturning(ctx, pi);
|
|
80
|
+
const result = await clearReturning(ctx, pi, session);
|
|
141
81
|
|
|
142
82
|
ctx.ui.notify(
|
|
143
83
|
result
|
|
@@ -150,8 +90,9 @@ export async function clear(
|
|
|
150
90
|
export async function clearReturning(
|
|
151
91
|
ctx: ExtensionCommandContext,
|
|
152
92
|
pi: ExtensionAPI,
|
|
93
|
+
session: ActivePresetSession,
|
|
153
94
|
): Promise<ClearResult | undefined> {
|
|
154
|
-
const active =
|
|
95
|
+
const active = session.current();
|
|
155
96
|
|
|
156
97
|
if (!active) return undefined;
|
|
157
98
|
|
|
@@ -165,11 +106,9 @@ export async function clearReturning(
|
|
|
165
106
|
currentThinking: pi.getThinkingLevel(),
|
|
166
107
|
currentTools: pi.getActiveTools(),
|
|
167
108
|
});
|
|
168
|
-
const finalParts = await executeClear(decision, ctx, pi);
|
|
109
|
+
const finalParts = await executeClear(decision, ctx, pi, session);
|
|
169
110
|
|
|
170
|
-
|
|
171
|
-
pi.appendEntry("presets-plus:active", { name: null });
|
|
172
|
-
updateStatus(ctx, getActive(), () => undefined);
|
|
111
|
+
session.clear(ctx, pi);
|
|
173
112
|
|
|
174
113
|
return { name: active.name, parts: finalParts };
|
|
175
114
|
}
|
|
@@ -293,32 +232,11 @@ export function decideClear(snapshot: ClearSnapshot): ClearDecision {
|
|
|
293
232
|
return { parts, writes };
|
|
294
233
|
}
|
|
295
234
|
|
|
296
|
-
export function renderClearSummary(
|
|
297
|
-
name: string,
|
|
298
|
-
parts: readonly ClearPart[],
|
|
299
|
-
styler: Pick<Theme, "bold" | "fg"> = IDENTITY_STYLER,
|
|
300
|
-
): string {
|
|
301
|
-
const safeStyler = normalizeStyler(styler);
|
|
302
|
-
const labels = parts.map((part) => `${FIELD_LABELS[part.field]}:`);
|
|
303
|
-
const labelWidth = Math.max(...labels.map((label) => label.length));
|
|
304
|
-
const title = safeStyler.bold(
|
|
305
|
-
safeStyler.fg("accent", `${CLEAR_DIALOG_TITLE}: ${name}`),
|
|
306
|
-
);
|
|
307
|
-
const lead = chooseClearLead(parts);
|
|
308
|
-
const rows = parts.map((part) => {
|
|
309
|
-
const label = `${FIELD_LABELS[part.field]}:`;
|
|
310
|
-
const padding = " ".repeat(labelWidth - label.length);
|
|
311
|
-
|
|
312
|
-
return ` ${safeStyler.fg("muted", label)}${padding} ${formatRowValue(part)}`;
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
return [title, lead, ...rows].join("\n");
|
|
316
|
-
}
|
|
317
|
-
|
|
318
235
|
async function executeClear(
|
|
319
236
|
decision: ClearDecision,
|
|
320
237
|
ctx: Pick<ExtensionCommandContext, "modelRegistry">,
|
|
321
238
|
pi: Pick<ExtensionAPI, "setActiveTools" | "setModel" | "setThinkingLevel">,
|
|
239
|
+
session: ActivePresetSession,
|
|
322
240
|
): Promise<ClearPart[]> {
|
|
323
241
|
const parts = decision.parts.map((part) => ({ ...part }));
|
|
324
242
|
|
|
@@ -326,7 +244,7 @@ async function executeClear(
|
|
|
326
244
|
const target = decision.writes.model;
|
|
327
245
|
const model = ctx.modelRegistry.find(target.provider, target.id);
|
|
328
246
|
const ok = model
|
|
329
|
-
? await withSelfTriggeredModelSet(() => pi.setModel(model))
|
|
247
|
+
? await session.withSelfTriggeredModelSet(() => pi.setModel(model))
|
|
330
248
|
: false;
|
|
331
249
|
|
|
332
250
|
if (!ok) {
|
|
@@ -353,79 +271,6 @@ async function executeClear(
|
|
|
353
271
|
return parts;
|
|
354
272
|
}
|
|
355
273
|
|
|
356
|
-
function formatModel(model: { provider: string; id: string } | null): string {
|
|
357
|
-
return model ? `${model.provider}/${model.id}` : "none";
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Render the post-colon body for a single field row.
|
|
362
|
-
*
|
|
363
|
-
* The vocabulary intentionally parallels `formatStatus` so a user reading
|
|
364
|
-
* `/presets status` and then `/presets clear <name>` sees the same phrases
|
|
365
|
-
* for manual overrides / "not managed by …" /
|
|
366
|
-
* "no baseline saved for this field". Restored / already-baseline rows
|
|
367
|
-
* stay bare — the lead sentence above the rows already explains the
|
|
368
|
-
* disposition.
|
|
369
|
-
*/
|
|
370
|
-
function formatRowValue(part: ClearPart): string {
|
|
371
|
-
switch (part.action) {
|
|
372
|
-
case "already-baseline":
|
|
373
|
-
case "restored":
|
|
374
|
-
return part.value;
|
|
375
|
-
|
|
376
|
-
case "baseline-null":
|
|
377
|
-
case "unknown":
|
|
378
|
-
return `${part.value} (No baseline saved for this field)`;
|
|
379
|
-
|
|
380
|
-
case "not-owned":
|
|
381
|
-
return `${part.value} (Not managed by cleared preset)`;
|
|
382
|
-
|
|
383
|
-
case "restore-failed":
|
|
384
|
-
return `Could not switch back to ${part.value}.`;
|
|
385
|
-
|
|
386
|
-
case "restored-partial":
|
|
387
|
-
return part.dropped && part.dropped.length > 0
|
|
388
|
-
? `${part.value} (Unavailable: ${part.dropped.join(", ")})`
|
|
389
|
-
: part.value;
|
|
390
|
-
|
|
391
|
-
case "user-override":
|
|
392
|
-
return `${part.value} (Left as-is — you changed it after activation)`;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
function formatTools(tools: readonly string[]): string {
|
|
397
|
-
return tools.length > 0 ? tools.join(", ") : "none";
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function isKeptLike(action: ClearAction): boolean {
|
|
401
|
-
return (
|
|
402
|
-
action === "user-override" ||
|
|
403
|
-
action === "not-owned" ||
|
|
404
|
-
action === "baseline-null"
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function isRestoreLike(action: ClearAction): boolean {
|
|
409
|
-
return (
|
|
410
|
-
action === "restored" ||
|
|
411
|
-
action === "restored-partial" ||
|
|
412
|
-
action === "already-baseline"
|
|
413
|
-
);
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function normalizeStyler(styler: Pick<Theme, "bold" | "fg">): Styler {
|
|
417
|
-
return {
|
|
418
|
-
bold: (text) =>
|
|
419
|
-
typeof styler.bold === "function"
|
|
420
|
-
? styler.bold(text)
|
|
421
|
-
: IDENTITY_STYLER.bold(text),
|
|
422
|
-
fg: (color, text) =>
|
|
423
|
-
typeof styler.fg === "function"
|
|
424
|
-
? styler.fg(color, text)
|
|
425
|
-
: IDENTITY_STYLER.fg(color, text),
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
274
|
function sameModel(
|
|
430
275
|
left: { provider: string; id: string } | null,
|
|
431
276
|
right: { provider: string; id: string } | null,
|
|
@@ -7,10 +7,8 @@
|
|
|
7
7
|
* `declared` snapshot cached on `ActivePresetState` at apply / restore time
|
|
8
8
|
* so per-turn handlers stay in-memory only.
|
|
9
9
|
*/
|
|
10
|
-
import { getActive } from "./active-state.js";
|
|
11
|
-
import { isSelfTriggeredModelSet } from "./apply.js";
|
|
12
|
-
import { markClean, markDirty } from "./dirty.js";
|
|
13
10
|
import { detectDriftReasons } from "./drift.js";
|
|
11
|
+
import type { ActivePresetSession } from "./session.js";
|
|
14
12
|
import type {
|
|
15
13
|
ExtensionAPI,
|
|
16
14
|
ExtensionContext,
|
|
@@ -43,29 +41,33 @@ export async function handleModelSelectDrift(
|
|
|
43
41
|
event: ModelSelectLikeEvent,
|
|
44
42
|
ctx: DriftHandlerContext,
|
|
45
43
|
pi: DriftHandlerPi,
|
|
44
|
+
session: ActivePresetSession,
|
|
46
45
|
): Promise<void> {
|
|
47
|
-
if (
|
|
46
|
+
if (session.isSelfTriggered()) return;
|
|
48
47
|
if (event.source === "restore") return;
|
|
49
48
|
|
|
50
|
-
await syncDirtyFromCurrentState(ctx, pi);
|
|
49
|
+
await syncDirtyFromCurrentState(ctx, pi, session);
|
|
51
50
|
}
|
|
52
51
|
|
|
53
52
|
/** Recompute all drift reasons and update the dirty flag if needed. */
|
|
54
53
|
export async function syncDirtyFromCurrentState(
|
|
55
54
|
ctx: DriftHandlerContext,
|
|
56
55
|
pi: DriftHandlerPi,
|
|
56
|
+
session: ActivePresetSession,
|
|
57
57
|
): Promise<void> {
|
|
58
|
-
const active =
|
|
58
|
+
const active = session.current();
|
|
59
59
|
|
|
60
60
|
if (!active) return;
|
|
61
61
|
|
|
62
62
|
const reasons = detectDriftReasons(active.declared, pi, ctx);
|
|
63
63
|
|
|
64
64
|
if (reasons.length === 0) {
|
|
65
|
-
if (active.dirty)
|
|
65
|
+
if (active.dirty) session.markClean(ctx);
|
|
66
66
|
|
|
67
67
|
return;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
if (!active.dirty)
|
|
70
|
+
if (!active.dirty) session.markDirty(ctx);
|
|
71
|
+
|
|
72
|
+
await Promise.resolve();
|
|
71
73
|
}
|