@quandev104/pi-style 0.1.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/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/extensions/pi-style.js +7897 -0
- package/dist/extensions/pi-style.js.map +1 -0
- package/extension-src/pi-style/app/command-service.ts +244 -0
- package/extension-src/pi-style/app/commands.ts +12 -0
- package/extension-src/pi-style/app/config-storage.ts +98 -0
- package/extension-src/pi-style/app/doctor.ts +53 -0
- package/extension-src/pi-style/app/index.ts +322 -0
- package/extension-src/pi-style/app/providers.ts +268 -0
- package/extension-src/pi-style/app/render-scheduler.ts +48 -0
- package/extension-src/pi-style/app/runtime.ts +404 -0
- package/extension-src/pi-style/app/snapshot.ts +15 -0
- package/extension-src/pi-style/domain/capabilities.ts +34 -0
- package/extension-src/pi-style/domain/config-authorization.ts +20 -0
- package/extension-src/pi-style/domain/config-diagnostics.ts +20 -0
- package/extension-src/pi-style/domain/config-diff.ts +30 -0
- package/extension-src/pi-style/domain/config-migrations.ts +52 -0
- package/extension-src/pi-style/domain/config-normalization.ts +567 -0
- package/extension-src/pi-style/domain/config-presets.ts +51 -0
- package/extension-src/pi-style/domain/config-types.ts +115 -0
- package/extension-src/pi-style/domain/providers.ts +37 -0
- package/extension-src/pi-style/domain/status-presets.ts +60 -0
- package/extension-src/pi-style/domain/status-renderer.ts +142 -0
- package/extension-src/pi-style/domain/status.ts +430 -0
- package/extension-src/pi-style/domain/theme.ts +232 -0
- package/extension-src/pi-style/features/.gitkeep +0 -0
- package/extension-src/pi-style/features/editor/index.ts +304 -0
- package/extension-src/pi-style/features/messages/boxed-block.ts +74 -0
- package/extension-src/pi-style/features/messages/index.ts +145 -0
- package/extension-src/pi-style/features/messages/special-blocks.ts +281 -0
- package/extension-src/pi-style/features/startup/index.ts +564 -0
- package/extension-src/pi-style/features/startup/logo.ts +151 -0
- package/extension-src/pi-style/features/status-line/index.ts +319 -0
- package/extension-src/pi-style/features/tools/boxed/bash.ts +347 -0
- package/extension-src/pi-style/features/tools/boxed/edit.ts +113 -0
- package/extension-src/pi-style/features/tools/boxed/fallback.ts +67 -0
- package/extension-src/pi-style/features/tools/boxed/find.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/grep.ts +108 -0
- package/extension-src/pi-style/features/tools/boxed/index.ts +64 -0
- package/extension-src/pi-style/features/tools/boxed/ls.ts +65 -0
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +190 -0
- package/extension-src/pi-style/features/tools/boxed/read.ts +204 -0
- package/extension-src/pi-style/features/tools/boxed/session-config.ts +45 -0
- package/extension-src/pi-style/features/tools/boxed/shared.ts +157 -0
- package/extension-src/pi-style/features/tools/boxed/write.ts +89 -0
- package/extension-src/pi-style/features/tools/index.ts +480 -0
- package/extension-src/pi-style/pi/commands.ts +17 -0
- package/extension-src/pi-style/pi/compatibility-coordinator.ts +195 -0
- package/extension-src/pi-style/pi/compatibility-probe.ts +645 -0
- package/extension-src/pi-style/pi/compatibility-registry.ts +329 -0
- package/extension-src/pi-style/pi/config-host.ts +52 -0
- package/extension-src/pi-style/pi/config-session.ts +105 -0
- package/extension-src/pi-style/pi/index.ts +77 -0
- package/extension-src/pi-style/pi/operational-state.ts +29 -0
- package/extension-src/pi-style/pi/session-coordinator.ts +187 -0
- package/extension-src/pi-style/pi/session-usage.ts +62 -0
- package/extension-src/pi-style/pi/startup-resources.ts +70 -0
- package/extension-src/pi-style/shared/.gitkeep +0 -0
- package/extension-src/pi-style/shared/ansi.ts +386 -0
- package/extension-src/pi-style/shared/box.ts +805 -0
- package/extension-src/pi-style/shared/disposable-store.ts +28 -0
- package/extension-src/pi-style/shared/elapsed.ts +88 -0
- package/extension-src/pi-style/shared/render-budget.ts +367 -0
- package/extension-src/pi-style/shared/split-diff.ts +692 -0
- package/extension-src/pi-style/shared/theme-extras.ts +292 -0
- package/package.json +68 -0
- package/themes/.gitkeep +0 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
export type CompatibilitySubtype =
|
|
2
|
+
| "native-user-message"
|
|
3
|
+
| "native-assistant-message"
|
|
4
|
+
| "native-compaction-message"
|
|
5
|
+
| "native-branch-message"
|
|
6
|
+
| "native-skill-message"
|
|
7
|
+
| "native-custom-message"
|
|
8
|
+
| "tool-call-renderer"
|
|
9
|
+
| "tool-result-renderer";
|
|
10
|
+
|
|
11
|
+
type CompatibilityShape = "supported" | "unsupported" | "conflict" | "installed" | "skipped";
|
|
12
|
+
|
|
13
|
+
export interface CompatibilityRecord {
|
|
14
|
+
feature: "messages" | "tools";
|
|
15
|
+
subtype: CompatibilitySubtype;
|
|
16
|
+
target: object;
|
|
17
|
+
owner: object;
|
|
18
|
+
method: PropertyKey;
|
|
19
|
+
originalIdentity: unknown;
|
|
20
|
+
originalDescriptor?: PropertyDescriptor;
|
|
21
|
+
installedIdentity?: unknown;
|
|
22
|
+
piVersion: string;
|
|
23
|
+
versionRange: string;
|
|
24
|
+
shape: CompatibilityShape;
|
|
25
|
+
diagnostic?: string | undefined;
|
|
26
|
+
generation: number;
|
|
27
|
+
disposed: boolean;
|
|
28
|
+
disposer: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface InstallResult {
|
|
32
|
+
status: "installed" | "already-installed" | "skipped";
|
|
33
|
+
record: CompatibilityRecord;
|
|
34
|
+
reason?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const recordsByOwner = new WeakMap<object, Map<PropertyKey, CompatibilityRecord>>();
|
|
38
|
+
let generation = 0;
|
|
39
|
+
let registryTestHooks: {
|
|
40
|
+
afterWrite?: () => void;
|
|
41
|
+
defineProperty?: (target: object, key: PropertyKey, descriptor: PropertyDescriptor) => boolean;
|
|
42
|
+
} = {};
|
|
43
|
+
export function __setCompatibilityRegistryTestHooks(hooks: typeof registryTestHooks): () => void {
|
|
44
|
+
const previous = registryTestHooks;
|
|
45
|
+
registryTestHooks = hooks;
|
|
46
|
+
return () => {
|
|
47
|
+
registryTestHooks = previous;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ownerRecords(owner: object): Map<PropertyKey, CompatibilityRecord> {
|
|
52
|
+
let records = recordsByOwner.get(owner);
|
|
53
|
+
if (!records) {
|
|
54
|
+
records = new Map();
|
|
55
|
+
recordsByOwner.set(owner, records);
|
|
56
|
+
}
|
|
57
|
+
return records;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function currentGeneration(): number {
|
|
61
|
+
return generation;
|
|
62
|
+
}
|
|
63
|
+
export function nextGeneration(): number {
|
|
64
|
+
return ++generation;
|
|
65
|
+
}
|
|
66
|
+
export function getCompatibilityRecords(owner: object): readonly CompatibilityRecord[] {
|
|
67
|
+
return [...(recordsByOwner.get(owner)?.values() ?? [])];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function safeRead(target: object, method: PropertyKey): unknown {
|
|
71
|
+
try {
|
|
72
|
+
return Reflect.get(target, method);
|
|
73
|
+
} catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function skippedRecord(options: {
|
|
79
|
+
feature: CompatibilityRecord["feature"];
|
|
80
|
+
subtype: CompatibilitySubtype;
|
|
81
|
+
target: object;
|
|
82
|
+
method: PropertyKey;
|
|
83
|
+
piVersion: string;
|
|
84
|
+
versionRange: string;
|
|
85
|
+
shape: CompatibilityShape;
|
|
86
|
+
diagnostic: string;
|
|
87
|
+
generation: number;
|
|
88
|
+
originalIdentity?: unknown;
|
|
89
|
+
}): CompatibilityRecord {
|
|
90
|
+
return {
|
|
91
|
+
...options,
|
|
92
|
+
owner: options.target,
|
|
93
|
+
originalIdentity: Object.hasOwn(options, "originalIdentity")
|
|
94
|
+
? options.originalIdentity
|
|
95
|
+
: safeRead(options.target, options.method),
|
|
96
|
+
disposed: true,
|
|
97
|
+
disposer: () => {},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function descriptorMatches(actual: PropertyDescriptor | undefined, expected: PropertyDescriptor | undefined): boolean {
|
|
102
|
+
if (!actual || !expected) return actual === expected;
|
|
103
|
+
return (
|
|
104
|
+
actual.value === expected.value &&
|
|
105
|
+
actual.get === expected.get &&
|
|
106
|
+
actual.set === expected.set &&
|
|
107
|
+
actual.writable === expected.writable &&
|
|
108
|
+
actual.enumerable === expected.enumerable &&
|
|
109
|
+
actual.configurable === expected.configurable
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function restoreExact(record: CompatibilityRecord): { ok: boolean; diagnostic?: string } {
|
|
114
|
+
try {
|
|
115
|
+
const current = Object.getOwnPropertyDescriptor(record.target, record.method);
|
|
116
|
+
const installed = current?.value === record.installedIdentity;
|
|
117
|
+
if (!installed) return { ok: false, diagnostic: "current owner changed; native/later owner preserved" };
|
|
118
|
+
const defineProperty = registryTestHooks.defineProperty ?? Reflect.defineProperty;
|
|
119
|
+
const wrote = record.originalDescriptor
|
|
120
|
+
? defineProperty(record.target, record.method, record.originalDescriptor)
|
|
121
|
+
: Reflect.deleteProperty(record.target, record.method);
|
|
122
|
+
if (!wrote) return { ok: false, diagnostic: "exact descriptor restoration was rejected" };
|
|
123
|
+
if (!descriptorMatches(Object.getOwnPropertyDescriptor(record.target, record.method), record.originalDescriptor))
|
|
124
|
+
return { ok: false, diagnostic: "post-restore descriptor validation failed" };
|
|
125
|
+
return { ok: true };
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return { ok: false, diagnostic: `restore failed: ${error instanceof Error ? error.message : String(error)}` };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function inspectCurrent(
|
|
132
|
+
target: object,
|
|
133
|
+
method: PropertyKey,
|
|
134
|
+
): { current?: unknown; descriptor?: PropertyDescriptor; error?: string } {
|
|
135
|
+
try {
|
|
136
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, method);
|
|
137
|
+
return {
|
|
138
|
+
current: descriptor && "value" in descriptor ? descriptor.value : undefined,
|
|
139
|
+
...(descriptor ? { descriptor } : {}),
|
|
140
|
+
};
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return { error: `cannot inspect target: ${error instanceof Error ? error.message : String(error)}` };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function skippedResult(
|
|
147
|
+
options: Parameters<typeof installDelegatingPatch>[0],
|
|
148
|
+
shape: CompatibilityShape,
|
|
149
|
+
reason: string,
|
|
150
|
+
originalIdentity?: unknown,
|
|
151
|
+
): InstallResult {
|
|
152
|
+
return {
|
|
153
|
+
status: "skipped",
|
|
154
|
+
reason,
|
|
155
|
+
record: skippedRecord({ ...options, shape, diagnostic: reason, originalIdentity }),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function expectedIdentityMatches(options: Parameters<typeof installDelegatingPatch>[0], current: unknown): boolean {
|
|
160
|
+
const expected =
|
|
161
|
+
options.hasExpectedIdentity === true ||
|
|
162
|
+
(options.hasExpectedIdentity === undefined && options.expectedIdentity !== undefined);
|
|
163
|
+
return !expected || current === options.expectedIdentity;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function validateInstall(
|
|
167
|
+
options: Parameters<typeof installDelegatingPatch>[0],
|
|
168
|
+
current: unknown,
|
|
169
|
+
existing: CompatibilityRecord | undefined,
|
|
170
|
+
ownDescriptor: PropertyDescriptor | undefined,
|
|
171
|
+
): InstallResult | undefined {
|
|
172
|
+
if (!options.shape) return skippedResult(options, "unsupported", options.diagnostic ?? "unsupported shape", current);
|
|
173
|
+
const conflict = existing && activeConflict(existing, current, options.generation);
|
|
174
|
+
if (conflict) return conflict;
|
|
175
|
+
if (typeof current !== "function")
|
|
176
|
+
return skippedResult(
|
|
177
|
+
options,
|
|
178
|
+
ownDescriptor ? "unsupported" : "skipped",
|
|
179
|
+
"target property is not callable",
|
|
180
|
+
current,
|
|
181
|
+
);
|
|
182
|
+
if (!expectedIdentityMatches(options, current))
|
|
183
|
+
return skippedResult(options, "conflict", "current owner is not the captured pristine native identity", current);
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function activeConflict(
|
|
188
|
+
existing: CompatibilityRecord,
|
|
189
|
+
current: unknown,
|
|
190
|
+
generation: number,
|
|
191
|
+
): InstallResult | undefined {
|
|
192
|
+
if (existing.disposed) return undefined;
|
|
193
|
+
if (existing.installedIdentity === current && existing.generation === generation)
|
|
194
|
+
return { status: "already-installed", record: existing };
|
|
195
|
+
const reason =
|
|
196
|
+
existing.installedIdentity === current
|
|
197
|
+
? "active wrapper belongs to a different generation"
|
|
198
|
+
: "owner changed after pi-style installation";
|
|
199
|
+
return {
|
|
200
|
+
status: "skipped",
|
|
201
|
+
reason,
|
|
202
|
+
record: skippedRecord({
|
|
203
|
+
feature: existing.feature,
|
|
204
|
+
subtype: existing.subtype,
|
|
205
|
+
target: existing.target,
|
|
206
|
+
method: existing.method,
|
|
207
|
+
piVersion: existing.piVersion,
|
|
208
|
+
versionRange: existing.versionRange,
|
|
209
|
+
shape: "conflict",
|
|
210
|
+
diagnostic: reason,
|
|
211
|
+
generation,
|
|
212
|
+
originalIdentity: current,
|
|
213
|
+
}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function installDelegatingPatch(options: {
|
|
218
|
+
feature: CompatibilityRecord["feature"];
|
|
219
|
+
subtype: CompatibilitySubtype;
|
|
220
|
+
target: object;
|
|
221
|
+
method: PropertyKey;
|
|
222
|
+
piVersion: string;
|
|
223
|
+
versionRange: string;
|
|
224
|
+
shape: boolean;
|
|
225
|
+
generation: number;
|
|
226
|
+
expectedIdentity?: unknown;
|
|
227
|
+
hasExpectedIdentity?: boolean;
|
|
228
|
+
diagnostic?: string | undefined;
|
|
229
|
+
delegate: (original: unknown, thisArg: object, args: unknown[]) => unknown;
|
|
230
|
+
}): InstallResult {
|
|
231
|
+
const { target, method } = options;
|
|
232
|
+
const inspection = inspectCurrent(target, method);
|
|
233
|
+
if (inspection.error) return skippedResult(options, "skipped", inspection.error);
|
|
234
|
+
const current = inspection.current;
|
|
235
|
+
const records = ownerRecords(target);
|
|
236
|
+
const validation = validateInstall(options, current, records.get(method), inspection.descriptor);
|
|
237
|
+
if (validation) return validation;
|
|
238
|
+
const originalDescriptor = Object.getOwnPropertyDescriptor(target, method);
|
|
239
|
+
const originalIdentity = current;
|
|
240
|
+
let active = true;
|
|
241
|
+
const installed = function (this: object, ...args: unknown[]): unknown {
|
|
242
|
+
if (!active) return Reflect.apply(originalIdentity as (...values: unknown[]) => unknown, this, args);
|
|
243
|
+
return options.delegate(originalIdentity, this, args);
|
|
244
|
+
};
|
|
245
|
+
const record: CompatibilityRecord = {
|
|
246
|
+
feature: options.feature,
|
|
247
|
+
subtype: options.subtype,
|
|
248
|
+
target,
|
|
249
|
+
owner: target,
|
|
250
|
+
method,
|
|
251
|
+
originalIdentity,
|
|
252
|
+
...(originalDescriptor ? { originalDescriptor } : {}),
|
|
253
|
+
installedIdentity: installed,
|
|
254
|
+
piVersion: options.piVersion,
|
|
255
|
+
versionRange: options.versionRange,
|
|
256
|
+
shape: "installed",
|
|
257
|
+
diagnostic: options.diagnostic,
|
|
258
|
+
generation: options.generation,
|
|
259
|
+
disposed: false,
|
|
260
|
+
disposer: () => {},
|
|
261
|
+
};
|
|
262
|
+
try {
|
|
263
|
+
Object.defineProperty(installed, "__piStyleCompatibilityRecord", { value: record, configurable: false });
|
|
264
|
+
const descriptor = { ...originalDescriptor, value: installed };
|
|
265
|
+
const wrote = Reflect.defineProperty(target, method, descriptor);
|
|
266
|
+
registryTestHooks.afterWrite?.();
|
|
267
|
+
const currentDescriptor = Object.getOwnPropertyDescriptor(target, method);
|
|
268
|
+
if (!wrote) return skippedResult(options, "skipped", "installation write was rejected");
|
|
269
|
+
if (!descriptorMatches(currentDescriptor, descriptor)) {
|
|
270
|
+
if (currentDescriptor?.value === installed) {
|
|
271
|
+
const rollback = restoreExact({ ...record, installedIdentity: installed });
|
|
272
|
+
if (!rollback.ok) {
|
|
273
|
+
record.diagnostic = rollback.diagnostic;
|
|
274
|
+
records.set(method, record);
|
|
275
|
+
return { status: "skipped", reason: rollback.diagnostic, record };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const reason =
|
|
279
|
+
currentDescriptor?.value === installed
|
|
280
|
+
? "wrapper-owned descriptor mismatch rolled back"
|
|
281
|
+
: "later owner preserved after post-write mismatch";
|
|
282
|
+
return {
|
|
283
|
+
status: "skipped",
|
|
284
|
+
reason,
|
|
285
|
+
record: skippedRecord({
|
|
286
|
+
...options,
|
|
287
|
+
shape: "conflict",
|
|
288
|
+
diagnostic: reason,
|
|
289
|
+
originalIdentity: currentDescriptor?.value,
|
|
290
|
+
}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
records.set(method, record);
|
|
294
|
+
record.disposer = () => {
|
|
295
|
+
if (record.disposed) return;
|
|
296
|
+
const restored = restoreExact(record);
|
|
297
|
+
if (!restored.ok) {
|
|
298
|
+
record.diagnostic = restored.diagnostic;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
active = false;
|
|
302
|
+
record.disposed = true;
|
|
303
|
+
records.delete(method);
|
|
304
|
+
};
|
|
305
|
+
return { status: "installed", record };
|
|
306
|
+
} catch (error) {
|
|
307
|
+
const currentDescriptor = Object.getOwnPropertyDescriptor(target, method);
|
|
308
|
+
const rollback =
|
|
309
|
+
currentDescriptor?.value === installed ? restoreExact({ ...record, installedIdentity: installed }) : { ok: true };
|
|
310
|
+
const reason = `installation rolled back (${rollback.diagnostic ?? "ok"}): ${error instanceof Error ? error.message : String(error)}`;
|
|
311
|
+
if (currentDescriptor?.value === installed && !rollback.ok) {
|
|
312
|
+
record.diagnostic = reason;
|
|
313
|
+
records.set(method, record);
|
|
314
|
+
record.disposer = () => {
|
|
315
|
+
if (record.disposed) return;
|
|
316
|
+
const retry = restoreExact(record);
|
|
317
|
+
if (!retry.ok) {
|
|
318
|
+
record.diagnostic = retry.diagnostic;
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
active = false;
|
|
322
|
+
record.disposed = true;
|
|
323
|
+
records.delete(method);
|
|
324
|
+
};
|
|
325
|
+
return { status: "installed", reason, record };
|
|
326
|
+
}
|
|
327
|
+
return { status: "skipped", reason, record: skippedRecord({ ...options, shape: "skipped", diagnostic: reason }) };
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { ConfigFilePort, ConfigStoragePaths } from "../app/config-storage.js";
|
|
5
|
+
|
|
6
|
+
const locks = new Map<string, Promise<void>>();
|
|
7
|
+
|
|
8
|
+
function serialize(path: string, operation: () => Promise<void>): Promise<void> {
|
|
9
|
+
const previous = locks.get(path) ?? Promise.resolve();
|
|
10
|
+
const current = previous.catch(() => undefined).then(operation);
|
|
11
|
+
locks.set(path, current);
|
|
12
|
+
return current.finally(() => {
|
|
13
|
+
if (locks.get(path) === current) locks.delete(path);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ConfigHostFs {
|
|
18
|
+
readonly mkdir?: typeof mkdir;
|
|
19
|
+
readonly readFile?: typeof readFile;
|
|
20
|
+
readonly rename?: typeof rename;
|
|
21
|
+
readonly unlink?: typeof unlink;
|
|
22
|
+
readonly writeFile?: typeof writeFile;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function createPiConfigFilePort(fs: ConfigHostFs = {}): ConfigFilePort {
|
|
26
|
+
const fsMkdir = fs.mkdir ?? mkdir;
|
|
27
|
+
const fsReadFile = fs.readFile ?? readFile;
|
|
28
|
+
const fsRename = fs.rename ?? rename;
|
|
29
|
+
const fsUnlink = fs.unlink ?? unlink;
|
|
30
|
+
const fsWriteFile = fs.writeFile ?? writeFile;
|
|
31
|
+
return {
|
|
32
|
+
read: (path) => fsReadFile(path, "utf8"),
|
|
33
|
+
writeAtomic: async (path, content) =>
|
|
34
|
+
serialize(path, async () => {
|
|
35
|
+
await fsMkdir(dirname(path), { recursive: true });
|
|
36
|
+
const temporary = `${path}.pi-style-${process.pid}-${Date.now()}.tmp`;
|
|
37
|
+
try {
|
|
38
|
+
await fsWriteFile(temporary, content, { mode: 0o600 });
|
|
39
|
+
await fsRename(temporary, path);
|
|
40
|
+
} finally {
|
|
41
|
+
await fsUnlink(temporary).catch(() => undefined);
|
|
42
|
+
}
|
|
43
|
+
}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function defaultStoragePaths(cwd: string, overrides: Partial<ConfigStoragePaths> = {}): ConfigStoragePaths {
|
|
48
|
+
return {
|
|
49
|
+
globalPath: overrides.globalPath ?? join(getAgentDir(), "settings.json"),
|
|
50
|
+
projectPath: overrides.projectPath ?? join(cwd, CONFIG_DIR_NAME, "settings.json"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ConfigFilePort } from "../app/config-storage.js";
|
|
2
|
+
import { readScopedConfig } from "../app/config-storage.js";
|
|
3
|
+
import type { ConfigDiagnostic } from "../domain/config-diagnostics.js";
|
|
4
|
+
import { resolveConfigDetailed } from "../domain/config-normalization.js";
|
|
5
|
+
|
|
6
|
+
export interface SessionFlagReader {
|
|
7
|
+
getFlag(name: string): unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface SessionAuthorization {
|
|
10
|
+
core: boolean;
|
|
11
|
+
user: boolean;
|
|
12
|
+
assistant: boolean;
|
|
13
|
+
specialBlocks: boolean;
|
|
14
|
+
tools: boolean;
|
|
15
|
+
ascii: boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ProductPolicy {
|
|
18
|
+
readonly corePatchGate: "omitted" | "allow" | "deny";
|
|
19
|
+
}
|
|
20
|
+
export interface ConfigSourceAdapter {
|
|
21
|
+
setSession(cwd: string, trusted: boolean): void;
|
|
22
|
+
load(): Promise<{
|
|
23
|
+
config: unknown;
|
|
24
|
+
diagnostics: readonly ConfigDiagnostic[];
|
|
25
|
+
sources: Readonly<Record<string, string>>;
|
|
26
|
+
rawSources?: {
|
|
27
|
+
defaults?: unknown;
|
|
28
|
+
global?: unknown;
|
|
29
|
+
project?: unknown;
|
|
30
|
+
environment?: Record<string, string | undefined>;
|
|
31
|
+
};
|
|
32
|
+
productPolicy?: ProductPolicy;
|
|
33
|
+
}>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sessionOverrides(_pi: SessionFlagReader): Record<string, unknown> {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Product policy is deny-only: any explicit false wins; true never grants Tier C. */
|
|
41
|
+
export function resolveProductGate(
|
|
42
|
+
global: unknown,
|
|
43
|
+
project: unknown,
|
|
44
|
+
session: unknown,
|
|
45
|
+
projectTrusted: boolean,
|
|
46
|
+
): ProductPolicy {
|
|
47
|
+
const values = [global, projectTrusted ? project : undefined, session];
|
|
48
|
+
for (const value of values) {
|
|
49
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) continue;
|
|
50
|
+
const compatibility = (value as Record<string, unknown>).compatibility;
|
|
51
|
+
if (!compatibility || typeof compatibility !== "object" || Array.isArray(compatibility)) continue;
|
|
52
|
+
if (!Object.hasOwn(compatibility, "allowCorePatches")) continue;
|
|
53
|
+
const leaf = (compatibility as Record<string, unknown>).allowCorePatches;
|
|
54
|
+
if (leaf === false) return { corePatchGate: "deny" };
|
|
55
|
+
}
|
|
56
|
+
return { corePatchGate: "omitted" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function readSessionAuthorization(pi: SessionFlagReader): SessionAuthorization {
|
|
60
|
+
return {
|
|
61
|
+
core: pi.getFlag("pi-style-core-patches") === true,
|
|
62
|
+
user: pi.getFlag("pi-style-message-user") === true,
|
|
63
|
+
assistant: pi.getFlag("pi-style-message-assistant") === true,
|
|
64
|
+
specialBlocks: pi.getFlag("pi-style-message-special-blocks") === true,
|
|
65
|
+
tools: pi.getFlag("pi-style-tools") === true,
|
|
66
|
+
ascii: pi.getFlag("pi-style-ascii") === true,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function createConfigSourceAdapter(
|
|
71
|
+
pi: SessionFlagReader,
|
|
72
|
+
port: ConfigFilePort,
|
|
73
|
+
paths: (cwd: string) => { globalPath: string; projectPath: string },
|
|
74
|
+
): ConfigSourceAdapter {
|
|
75
|
+
let trusted = true;
|
|
76
|
+
let currentCwd = process.cwd();
|
|
77
|
+
return {
|
|
78
|
+
setSession(nextCwd, nextTrusted) {
|
|
79
|
+
currentCwd = nextCwd;
|
|
80
|
+
trusted = nextTrusted;
|
|
81
|
+
},
|
|
82
|
+
async load() {
|
|
83
|
+
const storage = paths(currentCwd);
|
|
84
|
+
const global = await readScopedConfig(port, storage.globalPath);
|
|
85
|
+
const project = trusted ? await readScopedConfig(port, storage.projectPath) : undefined;
|
|
86
|
+
const resolved = resolveConfigDetailed({
|
|
87
|
+
global: global.value,
|
|
88
|
+
project: project?.value,
|
|
89
|
+
projectTrusted: trusted,
|
|
90
|
+
environment: process.env,
|
|
91
|
+
session: sessionOverrides(pi),
|
|
92
|
+
});
|
|
93
|
+
const productPolicy = resolveProductGate(global.value, project?.value, sessionOverrides(pi), trusted);
|
|
94
|
+
return {
|
|
95
|
+
config: resolved.config,
|
|
96
|
+
diagnostics: [...global.diagnostics, ...(project?.diagnostics ?? []), ...resolved.diagnostics],
|
|
97
|
+
sources: resolved.sources,
|
|
98
|
+
rawSources: { global: global.value, project: project?.value, environment: process.env },
|
|
99
|
+
productPolicy,
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export { sessionOverrides };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { StatusSnapshot } from "../domain/status.js";
|
|
3
|
+
import { registerPiStyleCommand } from "./commands.js";
|
|
4
|
+
import { type CompatibilityTestHooks, createPiStyleSessionCoordinator } from "./session-coordinator.js";
|
|
5
|
+
import { usageFromSession } from "./session-usage.js";
|
|
6
|
+
|
|
7
|
+
/** Usage patch that omits the key when no session usage exists (exact optional types). */
|
|
8
|
+
function usagePatch(ctx: ExtensionContext): StatusSnapshot {
|
|
9
|
+
const usage = usageFromSession(ctx.sessionManager);
|
|
10
|
+
return usage ? { usage } : {};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let compatibilityTestHooks: CompatibilityTestHooks = {};
|
|
14
|
+
export function __setCompatibilityTestHooks(hooks: CompatibilityTestHooks): () => void {
|
|
15
|
+
const previous = compatibilityTestHooks;
|
|
16
|
+
compatibilityTestHooks = hooks;
|
|
17
|
+
return () => {
|
|
18
|
+
compatibilityTestHooks = previous;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Thin Pi adapter: register flags, commands, and forward lifecycle events. */
|
|
23
|
+
export default function piStyleExtension(pi: ExtensionAPI): void {
|
|
24
|
+
// The core/message/tool surfaces are default-on (safe: fingerprint-certified against
|
|
25
|
+
// exact Pi 0.83.0, fail-closed elsewhere, conflict-preserving). The OFF switch is the
|
|
26
|
+
// product gate `compatibility.allowCorePatches: false` (or `enabled: false`) in config.
|
|
27
|
+
for (const [name, description] of [
|
|
28
|
+
["pi-style-core-patches", "Enable pi-style message/tool core patches"],
|
|
29
|
+
["pi-style-message-user", "Enable pi-style user message prefix"],
|
|
30
|
+
["pi-style-message-assistant", "Enable pi-style assistant message prefix"],
|
|
31
|
+
["pi-style-message-special-blocks", "Enable pi-style boxed compaction/skill/branch/custom message blocks"],
|
|
32
|
+
["pi-style-tools", "Enable pi-style tool renderer decoration"],
|
|
33
|
+
] as const)
|
|
34
|
+
pi.registerFlag(name, { type: "boolean", description, default: true });
|
|
35
|
+
// ASCII markers stay opt-in; unicode markers are the default.
|
|
36
|
+
pi.registerFlag("pi-style-ascii", { type: "boolean", description: "Use ASCII pi-style markers" });
|
|
37
|
+
const coordinator = createPiStyleSessionCoordinator(pi, compatibilityTestHooks);
|
|
38
|
+
registerPiStyleCommand(pi, coordinator.app);
|
|
39
|
+
pi.on("session_start", async (event, ctx) => {
|
|
40
|
+
await coordinator.start(event, ctx);
|
|
41
|
+
});
|
|
42
|
+
pi.on("agent_start", () => coordinator.app.runtime.current?.dismissStartup());
|
|
43
|
+
pi.on("input", () => coordinator.app.runtime.current?.dismissStartup());
|
|
44
|
+
pi.on("tool_execution_start", () => coordinator.app.runtime.current?.dismissStartup());
|
|
45
|
+
pi.on("model_select", (event) =>
|
|
46
|
+
coordinator.app.update(
|
|
47
|
+
{
|
|
48
|
+
model: event.model.name || event.model.id,
|
|
49
|
+
...(event.model.provider ? { provider: event.model.provider } : {}),
|
|
50
|
+
...(event.model.reasoning !== undefined ? { reasoning: event.model.reasoning } : {}),
|
|
51
|
+
},
|
|
52
|
+
"immediate",
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
pi.on("thinking_level_select", (event) => coordinator.app.update({ thinkingLevel: event.level }, "immediate"));
|
|
56
|
+
pi.on("session_info_changed", (event) => coordinator.app.update({ sessionName: event.name }, "coalesced"));
|
|
57
|
+
pi.on("message_update", () => coordinator.app.update({}, "coalesced"));
|
|
58
|
+
// Usage (tokens + cost) is aggregated from finalized session entries at
|
|
59
|
+
// message/turn boundaries, mirroring Pi's native footer; per-chunk updates
|
|
60
|
+
// stay usage-free to keep streaming cheap.
|
|
61
|
+
pi.on("message_end", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "coalesced"));
|
|
62
|
+
pi.on("turn_end", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "deferred"));
|
|
63
|
+
pi.on("agent_settled", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "coalesced"));
|
|
64
|
+
pi.on("session_tree", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "deferred"));
|
|
65
|
+
pi.on("session_compact", (_event, ctx) => coordinator.app.update({ ...usagePatch(ctx) }, "deferred"));
|
|
66
|
+
pi.on("tool_result", (event, ctx) => {
|
|
67
|
+
if (["write", "edit", "bash"].includes(event.toolName)) {
|
|
68
|
+
coordinator.app.runtime.current?.invalidateGit();
|
|
69
|
+
coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry");
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
pi.on("user_bash", (_event, ctx) => {
|
|
73
|
+
coordinator.app.runtime.current?.invalidateGit();
|
|
74
|
+
coordinator.app.update({ ...usagePatch(ctx) }, "delayed-retry");
|
|
75
|
+
});
|
|
76
|
+
pi.on("session_shutdown", () => coordinator.shutdown());
|
|
77
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { RuntimeInstallationState } from "../app/runtime.js";
|
|
2
|
+
import type { NormalizedPiStyleConfig } from "../domain/config-types.js";
|
|
3
|
+
import type { CompatibilityCoordinator } from "./compatibility-coordinator.js";
|
|
4
|
+
|
|
5
|
+
export function buildOperationalState(
|
|
6
|
+
config: NormalizedPiStyleConfig,
|
|
7
|
+
authorization: { core: boolean; productCorePatchGate?: string },
|
|
8
|
+
compatibility: CompatibilityCoordinator,
|
|
9
|
+
installationState?: RuntimeInstallationState,
|
|
10
|
+
productCorePatchGate?: string,
|
|
11
|
+
): Readonly<Record<string, unknown>> {
|
|
12
|
+
return {
|
|
13
|
+
compatibility: {
|
|
14
|
+
...compatibility.state(config),
|
|
15
|
+
configuredByProduct: config.messages.enabled || config.tools.enabled,
|
|
16
|
+
},
|
|
17
|
+
installations: installationState ?? {
|
|
18
|
+
status: config.enabled && config.statusLine.enabled ? "active" : "disabled",
|
|
19
|
+
editor: config.enabled && config.editor.enabled ? "active" : "disabled",
|
|
20
|
+
startup: config.enabled && config.startup.mode !== "off" ? "active" : "disabled",
|
|
21
|
+
},
|
|
22
|
+
provider: { status: "unavailable", recovery: "inject a capability-safe provider" },
|
|
23
|
+
authorization: {
|
|
24
|
+
core: authorization.core,
|
|
25
|
+
...(authorization.productCorePatchGate ? { productCorePatchGate: authorization.productCorePatchGate } : {}),
|
|
26
|
+
...(productCorePatchGate ? { productCorePatchGate } : {}),
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|