@danypops/pi-packed 0.19.4 → 0.19.6

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.
@@ -16,8 +16,8 @@ import type { Component } from "malevich-tui-components";
16
16
  import { shouldSearch } from "./discover-model.js";
17
17
  import type { Natives, PackageSummary } from "./packed.js";
18
18
  import { InstallServiceError } from "./packed.js";
19
- import { approvePackageOperation } from "./tools.js";
20
19
  import type { TabHost } from "./tab-host.js";
20
+ import { approvePackageOperation } from "./tools.js";
21
21
 
22
22
  const SEARCH_LIMIT = 20;
23
23
 
@@ -52,7 +52,10 @@ export async function applyInstall(result: PackageSummary, natives: Natives, ctx
52
52
  }
53
53
  }
54
54
 
55
- interface Theme { fg(color: string, s: string): string; bold(s: string): string; }
55
+ interface Theme {
56
+ fg(color: string, s: string): string;
57
+ bold(s: string): string;
58
+ }
56
59
 
57
60
  /** /packed's Find tab -- a real Component. Its query box always captures
58
61
  * free text (isCapturingInput() is unconditionally true), unlike
@@ -71,6 +74,7 @@ export class FindTab implements Component {
71
74
  constructor(
72
75
  private readonly natives: Natives,
73
76
  private readonly host: TabHost,
77
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: read via `const { theme } = this` in render(), which Biome's usage check doesn't trace
74
78
  private readonly theme: Theme,
75
79
  ) {}
76
80
 
@@ -169,7 +173,10 @@ export class FindTab implements Component {
169
173
  this.busy = true;
170
174
  this.host.requestRender();
171
175
  try {
172
- const confirmed = await this.host.inlineCtx.ui.confirm(`Install ${result.name}@${result.version}`, "Search itself never mutates -- this is the first of two confirmations before anything installs.");
176
+ const confirmed = await this.host.inlineCtx.ui.confirm(
177
+ `Install ${result.name}@${result.version}`,
178
+ "Search itself never mutates -- this is the first of two confirmations before anything installs.",
179
+ );
173
180
  if (!confirmed) return;
174
181
  const outcome = await applyInstall(result, this.natives, this.host.inlineCtx);
175
182
  if (outcome === "installed") this.host.onSessionReplaced();
@@ -9,13 +9,13 @@
9
9
  * Install: pi install git:github.com/DanyPops/pi-packed
10
10
  */
11
11
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
- import { registerTools } from "./tools.js";
13
- import { showPackedPanel } from "./tui.js";
14
- import { createNatives } from "./packed.js";
15
12
  import { formatUpdateNotice } from "./model.js";
13
+ import { createNatives } from "./packed.js";
16
14
  import { registerProfiles } from "./profile.js";
17
- import { handleSetupCommand } from "./setup-command.js";
18
15
  import { handleResourceConfigCommand } from "./resource-config.js";
16
+ import { handleSetupCommand } from "./setup-command.js";
17
+ import { registerTools } from "./tools.js";
18
+ import { showPackedPanel } from "./tui.js";
19
19
 
20
20
  // Async factory (pi awaits it): the seam creates authenticated daemon
21
21
  // clients lazily. It never executes Bun-only adapters or opens SQLite.
@@ -25,7 +25,8 @@ export default async function (pi: ExtensionAPI) {
25
25
  registerTools(pi, natives);
26
26
 
27
27
  pi.registerCommand("packed", {
28
- description: "Browse and manage installed Pi packages -- u/U update, x remove, d disable, c config, f find, s settings -- or run setup plan/apply or config directly",
28
+ description:
29
+ "Browse and manage installed Pi packages -- u/U update, x remove, d disable, c config, f find, s settings -- or run setup plan/apply or config directly",
29
30
  handler: async (args, ctx) => {
30
31
  if (await handleSetupCommand(args, ctx, natives)) return;
31
32
  if (await handleResourceConfigCommand(args, ctx, natives, showPackedPanel)) return;
@@ -9,13 +9,27 @@
9
9
  * that on the failing call itself and reconnects, so the happy path reuses
10
10
  * one connected client instead of reconnecting on every single operation.
11
11
  */
12
- import { createRetryingClient } from "@danypops/vehicle-client/daemon-client";
13
- import { ensurePackedClient, InstallServiceError, type PackedExtensionClient } from "@danypops/packed/client";
14
- import { PI_COMMAND_NAME, type InstalledPackage, type PackageInfo, type PackageResources, type PackageSummary, type PiStatus, type ResourceField, type SecuritySettings, type ServiceSpecSummary, type SetupApplyResult, type SetupPlan, type UpdateEntry, type UpdateOutcome } from "@danypops/packed/protocol";
15
12
 
16
- export { InstallServiceError, PI_COMMAND_NAME };
13
+ import { ensurePackedClient, InstallServiceError, type PackedExtensionClient } from "@danypops/packed/client";
14
+ import {
15
+ type InstalledPackage,
16
+ type PackageInfo,
17
+ type PackageResources,
18
+ type PackageSummary,
19
+ PI_COMMAND_NAME,
20
+ type PiStatus,
21
+ type ResourceField,
22
+ type SecuritySettings,
23
+ type ServiceSpecSummary,
24
+ type SetupApplyResult,
25
+ type SetupPlan,
26
+ type UpdateEntry,
27
+ type UpdateOutcome,
28
+ } from "@danypops/packed/protocol";
29
+ import { createRetryingClient } from "@danypops/vehicle-client/daemon-client";
17
30
 
18
31
  export type { PackageInfo, PackageResources, PackageSummary, PiStatus, ResourceField, UpdateEntry, UpdateOutcome };
32
+ export { InstallServiceError, PI_COMMAND_NAME };
19
33
  export type InstalledPkg = InstalledPackage;
20
34
  export type PackageDaemonPort = PackedExtensionClient;
21
35
  type MutationApproval = SecuritySettings["mutationApproval"];
@@ -42,7 +56,14 @@ export interface Natives {
42
56
  setupPlan(manifestPath: string, prune?: boolean): Promise<SetupPlan>;
43
57
  setupApply(manifestPath: string, approved?: boolean, prune?: boolean): Promise<SetupApplyResult>;
44
58
  listResources(projectRoot?: string): Promise<{ global: PackageResources[]; project: PackageResources[] }>;
45
- toggleResource(source: string, field: ResourceField, path: string, enabled: boolean, projectRoot?: string, approved?: boolean): Promise<string>;
59
+ toggleResource(
60
+ source: string,
61
+ field: ResourceField,
62
+ path: string,
63
+ enabled: boolean,
64
+ projectRoot?: string,
65
+ approved?: boolean,
66
+ ): Promise<string>;
46
67
  piStatus(): Promise<PiStatus>;
47
68
  }
48
69
 
@@ -70,7 +91,8 @@ export async function createNatives(connect: PackageDaemonConnector = connectDef
70
91
  setupPlan: (manifestPath, prune) => client.call((daemon) => daemon.setupPlan(manifestPath, prune)),
71
92
  setupApply: (manifestPath, approved, prune) => client.call((daemon) => daemon.setupApply(manifestPath, approved, prune)),
72
93
  listResources: (projectRoot) => client.call((daemon) => daemon.listResources(projectRoot)),
73
- toggleResource: (source, field, path, enabled, projectRoot, approved) => client.call((daemon) => daemon.toggleResource(source, field, path, enabled, projectRoot, approved)),
94
+ toggleResource: (source, field, path, enabled, projectRoot, approved) =>
95
+ client.call((daemon) => daemon.toggleResource(source, field, path, enabled, projectRoot, approved)),
74
96
  piStatus: () => client.call((daemon) => daemon.piStatus()),
75
97
  };
76
98
  }
@@ -1,7 +1,7 @@
1
- import { existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
- import { randomUUID } from "node:crypto";
1
+ import { existsSync, lstatSync, mkdirSync, readFileSync, rmSync } from "node:fs";
3
2
  import { join } from "node:path";
4
- import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { writeJsonAtomic } from "@danypops/packed/atomic-json";
4
+ import { CONFIG_DIR_NAME, type ExtensionAPI, type ExtensionContext, getAgentDir } from "@earendil-works/pi-coding-agent";
5
5
  import { Key } from "@earendil-works/pi-tui";
6
6
  export interface Profile {
7
7
  provider?: string;
@@ -30,14 +30,21 @@ function boundedString(value: unknown, maximum: number): string | undefined {
30
30
  }
31
31
 
32
32
  function secretLike(value: string): boolean {
33
- return /-----BEGIN [A-Z ]*PRIVATE KEY-----/i.test(value)
34
- || /\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|password|passwd|secret)\s*[:=]\s*["']?[A-Za-z0-9_\-./+=]{12,}/i.test(value)
35
- || /\b(?:authorization\s*:\s*)?bearer\s+[A-Za-z0-9_\-./+=]{12,}/i.test(value);
33
+ return (
34
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/i.test(value) ||
35
+ /\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|password|passwd|secret)\s*[:=]\s*["']?[A-Za-z0-9_\-./+=]{12,}/i.test(value) ||
36
+ /\b(?:authorization\s*:\s*)?bearer\s+[A-Za-z0-9_\-./+=]{12,}/i.test(value)
37
+ );
36
38
  }
37
39
 
38
40
  function stringArray(value: unknown, maximum: number): string[] | undefined {
39
41
  if (value === undefined) return undefined;
40
- if (!Array.isArray(value) || value.length > maximum || !value.every((item) => typeof item === "string" && item.length > 0 && item.length <= 256)) throw new Error("array must contain bounded strings");
42
+ if (
43
+ !Array.isArray(value) ||
44
+ value.length > maximum ||
45
+ !value.every((item) => typeof item === "string" && item.length > 0 && item.length <= 256)
46
+ )
47
+ throw new Error("array must contain bounded strings");
41
48
  return [...new Set(value)];
42
49
  }
43
50
 
@@ -52,16 +59,19 @@ export function decodeProfiles(text: string): ProfilesConfig {
52
59
  if (Object.keys(raw).some((key) => !allowed.has(key))) throw new Error(`profile ${name} contains an unknown field`);
53
60
  const profile: Profile = {};
54
61
  for (const field of ["provider", "model", "theme"] as const) {
55
- if (raw[field] !== undefined && !boundedString(raw[field], field === "model" ? 256 : 128)) throw new Error(`profile ${name}.${field} is invalid`);
62
+ if (raw[field] !== undefined && !boundedString(raw[field], field === "model" ? 256 : 128))
63
+ throw new Error(`profile ${name}.${field} is invalid`);
56
64
  const fieldValue = boundedString(raw[field], field === "model" ? 256 : 128);
57
65
  if (fieldValue) profile[field] = fieldValue;
58
66
  }
59
- if (raw.instructions !== undefined && !boundedString(raw.instructions, 16_384)) throw new Error(`profile ${name}.instructions is invalid`);
67
+ if (raw.instructions !== undefined && !boundedString(raw.instructions, 16_384))
68
+ throw new Error(`profile ${name}.instructions is invalid`);
60
69
  if (typeof raw.instructions === "string") {
61
70
  if (secretLike(raw.instructions)) throw new Error(`profile ${name}.instructions contains secret-like material`);
62
71
  profile.instructions = raw.instructions;
63
72
  }
64
- if (raw.thinkingLevel !== undefined && (typeof raw.thinkingLevel !== "string" || !THINKING_LEVELS.has(raw.thinkingLevel))) throw new Error(`profile ${name}.thinkingLevel is invalid`);
73
+ if (raw.thinkingLevel !== undefined && (typeof raw.thinkingLevel !== "string" || !THINKING_LEVELS.has(raw.thinkingLevel)))
74
+ throw new Error(`profile ${name}.thinkingLevel is invalid`);
65
75
  if (raw.thinkingLevel) profile.thinkingLevel = raw.thinkingLevel as ThinkingLevel;
66
76
  profile.tools = stringArray(raw.tools, 100);
67
77
  profile.allowedModels = stringArray(raw.allowedModels, 100);
@@ -75,7 +85,8 @@ export function decodeProfiles(text: string): ProfilesConfig {
75
85
  function readProfileFile(path: string): ProfilesConfig {
76
86
  if (!existsSync(path)) return {};
77
87
  const stat = lstatSync(path);
78
- if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_PROFILE_BYTES) throw new Error(`${path} is not a safe bounded profile file`);
88
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_PROFILE_BYTES)
89
+ throw new Error(`${path} is not a safe bounded profile file`);
79
90
  return decodeProfiles(readFileSync(path, "utf8"));
80
91
  }
81
92
 
@@ -93,8 +104,12 @@ function loadDefaultProfile(cwd: string, projectTrusted: boolean): string | unde
93
104
  const stat = lstatSync(path);
94
105
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 * 1024) return undefined;
95
106
  const value = JSON.parse(readFileSync(path, "utf8")) as { schemaVersion?: unknown; defaultProfile?: unknown };
96
- return value.schemaVersion === 1 && typeof value.defaultProfile === "string" && PROFILE_NAME.test(value.defaultProfile) ? value.defaultProfile : undefined;
97
- } catch { return undefined; }
107
+ return value.schemaVersion === 1 && typeof value.defaultProfile === "string" && PROFILE_NAME.test(value.defaultProfile)
108
+ ? value.defaultProfile
109
+ : undefined;
110
+ } catch {
111
+ return undefined;
112
+ }
98
113
  }
99
114
 
100
115
  function loadLastProfile(agentDir: string): string | undefined {
@@ -105,22 +120,28 @@ function loadLastProfile(agentDir: string): string | undefined {
105
120
  if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 4_096) return undefined;
106
121
  const value = JSON.parse(readFileSync(path, "utf8")) as { name?: unknown };
107
122
  return typeof value.name === "string" && PROFILE_NAME.test(value.name) ? value.name : undefined;
108
- } catch { return undefined; }
123
+ } catch {
124
+ return undefined;
125
+ }
109
126
  }
110
127
 
111
- function saveLastProfile(agentDir: string, name: string | undefined): void {
128
+ async function saveLastProfile(agentDir: string, name: string | undefined): Promise<void> {
112
129
  mkdirSync(agentDir, { recursive: true, mode: 0o700 });
113
130
  const path = join(agentDir, LAST_PROFILE_FILE);
114
- if (!name) { rmSync(path, { force: true }); return; }
115
- if (existsSync(path) && lstatSync(path).isSymbolicLink()) throw new Error("last-profile state is a symlink");
116
- const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
117
- try { writeFileSync(temporary, `${JSON.stringify({ name })}\n`, { flag: "wx", mode: 0o600 }); renameSync(temporary, path); }
118
- finally { rmSync(temporary, { force: true }); }
131
+ if (!name) {
132
+ rmSync(path, { force: true });
133
+ return;
134
+ }
135
+ await writeJsonAtomic(path, { name }, { mode: 0o600 });
119
136
  }
120
137
 
121
138
  function matchesPattern(value: string, pattern: string): boolean {
122
139
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
123
- try { return new RegExp(`^${escaped}$`).test(value); } catch { return false; }
140
+ try {
141
+ return new RegExp(`^${escaped}$`).test(value);
142
+ } catch {
143
+ return false;
144
+ }
124
145
  }
125
146
 
126
147
  interface OriginalState {
@@ -147,15 +168,19 @@ export function registerProfiles(pi: ExtensionAPI): void {
147
168
  }
148
169
 
149
170
  async function apply(name: string, profile: Profile, ctx: ExtensionContext, persist = true): Promise<void> {
150
- if (!original) original = { model: ctx.model, thinkingLevel: pi.getThinkingLevel(), tools: pi.getActiveTools(), themeName: ctx.ui.theme.name };
171
+ if (!original)
172
+ original = { model: ctx.model, thinkingLevel: pi.getThinkingLevel(), tools: pi.getActiveTools(), themeName: ctx.ui.theme.name };
151
173
  applyingModel = true;
152
174
  try {
153
175
  if (profile.provider && profile.model) {
154
176
  const model = ctx.modelRegistry.find(profile.provider, profile.model);
155
177
  if (!model) ctx.ui.notify(`Profile "${name}": model ${profile.provider}/${profile.model} not found`, "warning");
156
- else if (!await pi.setModel(model)) ctx.ui.notify(`Profile "${name}": credentials unavailable for ${profile.provider}/${profile.model}`, "warning");
178
+ else if (!(await pi.setModel(model)))
179
+ ctx.ui.notify(`Profile "${name}": credentials unavailable for ${profile.provider}/${profile.model}`, "warning");
157
180
  }
158
- } finally { applyingModel = false; }
181
+ } finally {
182
+ applyingModel = false;
183
+ }
159
184
  if (profile.thinkingLevel) pi.setThinkingLevel(profile.thinkingLevel);
160
185
  if (profile.tools?.length) {
161
186
  const available = new Set(pi.getAllTools().map((tool) => tool.name));
@@ -171,7 +196,11 @@ export function registerProfiles(pi: ExtensionAPI): void {
171
196
  activeName = name;
172
197
  activeProfile = profile;
173
198
  if (persist) {
174
- try { saveLastProfile(agentDir, name); } catch (error) { ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning"); }
199
+ try {
200
+ await saveLastProfile(agentDir, name);
201
+ } catch (error) {
202
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
203
+ }
175
204
  persistState(name);
176
205
  }
177
206
  setStatus(ctx);
@@ -190,28 +219,46 @@ export function registerProfiles(pi: ExtensionAPI): void {
190
219
  }
191
220
  }
192
221
  original = undefined;
193
- try { saveLastProfile(agentDir, undefined); } catch (error) { ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning"); }
222
+ try {
223
+ await saveLastProfile(agentDir, undefined);
224
+ } catch (error) {
225
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "warning");
226
+ }
194
227
  persistState(undefined);
195
228
  setStatus(ctx);
196
229
  }
197
230
 
198
231
  async function choose(ctx: ExtensionContext): Promise<void> {
199
232
  const names = Object.keys(profiles).sort();
200
- if (names.length === 0) { ctx.ui.notify("No profiles defined", "warning"); return; }
233
+ if (names.length === 0) {
234
+ ctx.ui.notify("No profiles defined", "warning");
235
+ return;
236
+ }
201
237
  const none = "(none)";
202
238
  const selected = await ctx.ui.select("Select profile", [none, ...names]);
203
239
  if (!selected) return;
204
- if (selected === none) { await clear(ctx); ctx.ui.notify("Profile cleared", "info"); return; }
240
+ if (selected === none) {
241
+ await clear(ctx);
242
+ ctx.ui.notify("Profile cleared", "info");
243
+ return;
244
+ }
205
245
  await apply(selected, profiles[selected]!, ctx);
206
246
  ctx.ui.notify(`Profile "${selected}" activated`, "info");
207
247
  }
208
248
 
209
249
  async function cycle(ctx: ExtensionContext): Promise<void> {
210
- if (Object.keys(profiles).length === 0) { ctx.ui.notify("No profiles defined", "warning"); return; }
250
+ if (Object.keys(profiles).length === 0) {
251
+ ctx.ui.notify("No profiles defined", "warning");
252
+ return;
253
+ }
211
254
  const values = [undefined, ...Object.keys(profiles).sort()];
212
255
  const index = values.indexOf(activeName);
213
256
  const next = values[(index + 1) % values.length];
214
- if (!next) { await clear(ctx); ctx.ui.notify("Profile cleared", "info"); return; }
257
+ if (!next) {
258
+ await clear(ctx);
259
+ ctx.ui.notify("Profile cleared", "info");
260
+ return;
261
+ }
215
262
  await apply(next, profiles[next]!, ctx);
216
263
  ctx.ui.notify(`Profile "${next}" activated`, "info");
217
264
  }
@@ -220,14 +267,23 @@ export function registerProfiles(pi: ExtensionAPI): void {
220
267
  pi.registerCommand("profile", {
221
268
  description: "Switch Packed profile",
222
269
  getArgumentCompletions(prefix) {
223
- const matches = Object.keys(profiles).sort().filter((name) => name.startsWith(prefix)).map((name) => ({ value: name, label: name }));
270
+ const matches = Object.keys(profiles)
271
+ .sort()
272
+ .filter((name) => name.startsWith(prefix))
273
+ .map((name) => ({ value: name, label: name }));
224
274
  return matches.length ? matches : null;
225
275
  },
226
276
  async handler(args, ctx) {
227
277
  const name = args.trim();
228
- if (!name) { await choose(ctx); return; }
278
+ if (!name) {
279
+ await choose(ctx);
280
+ return;
281
+ }
229
282
  const profile = profiles[name];
230
- if (!profile) { ctx.ui.notify(`Unknown profile "${name}"`, "error"); return; }
283
+ if (!profile) {
284
+ ctx.ui.notify(`Unknown profile "${name}"`, "error");
285
+ return;
286
+ }
231
287
  await apply(name, profile, ctx);
232
288
  ctx.ui.notify(`Profile "${name}" activated`, "info");
233
289
  },
@@ -237,15 +293,26 @@ export function registerProfiles(pi: ExtensionAPI): void {
237
293
  pi.on("model_select", (event, ctx) => {
238
294
  if (applyingModel || event.source === "restore" || !activeProfile?.allowedModels?.length) return;
239
295
  const selected = `${event.model.provider}/${event.model.id}`;
240
- if (!activeProfile.allowedModels.some((pattern) => matchesPattern(selected, pattern))) ctx.ui.notify(`${selected} is outside profile "${activeName}" allowedModels`, "warning");
296
+ if (!activeProfile.allowedModels.some((pattern) => matchesPattern(selected, pattern)))
297
+ ctx.ui.notify(`${selected} is outside profile "${activeName}" allowedModels`, "warning");
241
298
  });
242
- pi.on("before_agent_start", (event) => activeProfile?.instructions ? { systemPrompt: `${event.systemPrompt}\n\n${activeProfile.instructions}` } : undefined);
299
+ pi.on("before_agent_start", (event) =>
300
+ activeProfile?.instructions ? { systemPrompt: `${event.systemPrompt}\n\n${activeProfile.instructions}` } : undefined,
301
+ );
243
302
  pi.on("session_start", async (event, ctx) => {
244
- try { profiles = loadProfiles(agentDir, ctx.cwd, ctx.isProjectTrusted()); }
245
- catch (error) { profiles = {}; ctx.ui.notify(`Profile configuration rejected: ${error instanceof Error ? error.message : String(error)}`, "error"); }
303
+ try {
304
+ profiles = loadProfiles(agentDir, ctx.cwd, ctx.isProjectTrusted());
305
+ } catch (error) {
306
+ profiles = {};
307
+ ctx.ui.notify(`Profile configuration rejected: ${error instanceof Error ? error.message : String(error)}`, "error");
308
+ }
246
309
  const flag = pi.getFlag("profile");
247
310
  if (typeof flag === "string" && flag) {
248
- if (!profiles[flag]) { ctx.ui.notify(`Unknown profile "${flag}"`, "warning"); setStatus(ctx); return; }
311
+ if (!profiles[flag]) {
312
+ ctx.ui.notify(`Unknown profile "${flag}"`, "warning");
313
+ setStatus(ctx);
314
+ return;
315
+ }
249
316
  await apply(flag, profiles[flag]!, ctx);
250
317
  ctx.ui.notify(`Profile "${flag}" activated`, "info");
251
318
  return;
@@ -253,10 +320,18 @@ export function registerProfiles(pi: ExtensionAPI): void {
253
320
  const defaultProfile = loadDefaultProfile(ctx.cwd, ctx.isProjectTrusted());
254
321
  if ((event.reason === "reload" || event.reason === "new") && defaultProfile) {
255
322
  if (profiles[defaultProfile]) await apply(defaultProfile, profiles[defaultProfile]!, ctx, false);
256
- else { ctx.ui.notify(`Default profile "${defaultProfile}" is not defined`, "warning"); setStatus(ctx); }
323
+ else {
324
+ ctx.ui.notify(`Default profile "${defaultProfile}" is not defined`, "warning");
325
+ setStatus(ctx);
326
+ }
257
327
  return;
258
328
  }
259
- const state = [...ctx.sessionManager.getEntries()].reverse().find((entry: { type: string; customType?: string }) => entry.type === "custom" && (entry.customType === "profile-state" || entry.customType === "packed-profile-state")) as { data?: { name?: unknown } } | undefined;
329
+ const state = [...ctx.sessionManager.getEntries()]
330
+ .reverse()
331
+ .find(
332
+ (entry: { type: string; customType?: string }) =>
333
+ entry.type === "custom" && (entry.customType === "profile-state" || entry.customType === "packed-profile-state"),
334
+ ) as { data?: { name?: unknown } } | undefined;
260
335
  if (state) {
261
336
  if (typeof state.data?.name === "string" && profiles[state.data.name]) {
262
337
  activeName = state.data.name;
@@ -267,12 +342,17 @@ export function registerProfiles(pi: ExtensionAPI): void {
267
342
  }
268
343
  if (defaultProfile) {
269
344
  if (profiles[defaultProfile]) await apply(defaultProfile, profiles[defaultProfile]!, ctx, false);
270
- else { ctx.ui.notify(`Default profile "${defaultProfile}" is not defined`, "warning"); setStatus(ctx); }
345
+ else {
346
+ ctx.ui.notify(`Default profile "${defaultProfile}" is not defined`, "warning");
347
+ setStatus(ctx);
348
+ }
271
349
  return;
272
350
  }
273
351
  const last = loadLastProfile(agentDir);
274
- if (last && profiles[last]) { await apply(last, profiles[last]!, ctx, false); ctx.ui.notify(`Profile "${last}" restored`, "info"); }
275
- else {
352
+ if (last && profiles[last]) {
353
+ await apply(last, profiles[last]!, ctx, false);
354
+ ctx.ui.notify(`Profile "${last}" restored`, "info");
355
+ } else {
276
356
  if (last) ctx.ui.notify(`Last active profile "${last}" no longer exists`, "warning");
277
357
  setStatus(ctx);
278
358
  }
@@ -17,8 +17,7 @@ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
17
17
  import { rawKeyHint } from "@earendil-works/pi-coding-agent";
18
18
  import { Input, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
19
19
  import type { Component } from "malevich-tui-components";
20
- import type { PackageResources, ResourceField } from "./packed.js";
21
- import type { Natives } from "./packed.js";
20
+ import type { Natives, PackageResources, ResourceField } from "./packed.js";
22
21
  import { packagePermissionDecision } from "./permission.js";
23
22
  import { confirmReload } from "./reload.js";
24
23
  import type { TabHost } from "./tab-host.js";
@@ -39,7 +38,8 @@ export function flatten(groups: PackageResources[], scope: Scope): FlatItem[] {
39
38
  const items: FlatItem[] = [];
40
39
  for (const group of groups) {
41
40
  for (const field of RESOURCE_FIELDS) {
42
- for (const item of group[field]) items.push({ scope, source: group.source, packageName: group.name, field, path: item.path, enabled: item.enabled });
41
+ for (const item of group[field])
42
+ items.push({ scope, source: group.source, packageName: group.name, field, path: item.path, enabled: item.enabled });
43
43
  }
44
44
  }
45
45
  return items.sort((a, b) => a.packageName.localeCompare(b.packageName) || a.field.localeCompare(b.field) || a.path.localeCompare(b.path));
@@ -77,10 +77,17 @@ export type ToggleOutcome = "toggled" | "cancelled" | "failed";
77
77
  * actually reached the daemon. Extracted from the render loop so this
78
78
  * approve-then-mutate decision is directly testable without faking a full
79
79
  * ctx.ui.custom interaction. */
80
- export async function applyResourceToggle(item: FlatItem, natives: Pick<Natives, "security" | "toggleResource">, ctx: ExtensionCommandContext): Promise<ToggleOutcome> {
80
+ export async function applyResourceToggle(
81
+ item: FlatItem,
82
+ natives: Pick<Natives, "security" | "toggleResource">,
83
+ ctx: ExtensionCommandContext,
84
+ ): Promise<ToggleOutcome> {
81
85
  const nextEnabled = !item.enabled;
82
86
  const approved = await approveToggle(natives, ctx, item, nextEnabled);
83
- if (!approved) { ctx.ui.notify("toggle cancelled", "warning"); return "cancelled"; }
87
+ if (!approved) {
88
+ ctx.ui.notify("toggle cancelled", "warning");
89
+ return "cancelled";
90
+ }
84
91
  try {
85
92
  await natives.toggleResource(item.source, item.field, item.path, nextEnabled, item.scope === "project" ? ctx.cwd : undefined, true);
86
93
  return "toggled";
@@ -106,7 +113,10 @@ export async function handleResourceConfigCommand(
106
113
  return true;
107
114
  }
108
115
 
109
- interface Theme { fg(color: string, s: string): string; bold(s: string): string; }
116
+ interface Theme {
117
+ fg(color: string, s: string): string;
118
+ bold(s: string): string;
119
+ }
110
120
 
111
121
  /** /packed's Config tab -- a real Component, not its own ctx.ui.custom
112
122
  * overlay. Each toggle's reload decision (confirmReload) fires
@@ -129,6 +139,7 @@ export class ConfigTab implements Component {
129
139
  constructor(
130
140
  private readonly natives: Pick<Natives, "listResources" | "security" | "toggleResource">,
131
141
  private readonly host: TabHost,
142
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: read via `const { theme } = this` in render(), which Biome's usage check doesn't trace
132
143
  private readonly theme: Theme,
133
144
  initialFilter?: string,
134
145
  ) {
@@ -167,10 +178,20 @@ export class ConfigTab implements Component {
167
178
  const scopeLabel = theme.bold(this.scope === "project" ? "Project Resources" : "Global Resources");
168
179
  const hint = this.searchActive
169
180
  ? rawKeyHint("esc", "clear")
170
- : rawKeyHint("space", "toggle") + theme.fg("muted", " · ") + rawKeyHint("/", "filter") + theme.fg("muted", " · ") + rawKeyHint("v", "scope") + theme.fg("muted", " · ") + rawKeyHint("r", "refresh");
181
+ : rawKeyHint("space", "toggle") +
182
+ theme.fg("muted", " · ") +
183
+ rawKeyHint("/", "filter") +
184
+ theme.fg("muted", " · ") +
185
+ rawKeyHint("v", "scope") +
186
+ theme.fg("muted", " · ") +
187
+ rawKeyHint("r", "refresh");
171
188
  const spacing = Math.max(1, width - visibleWidth(scopeLabel) - visibleWidth(hint));
172
189
  const line1 = truncateToWidth(`${scopeLabel}${" ".repeat(spacing)}${hint}`, width, "");
173
- const line2 = truncateToWidth(theme.fg("muted", `${this.items.length} resource(s) \u00b7 extensions need a reload after a change`), width, "");
190
+ const line2 = truncateToWidth(
191
+ theme.fg("muted", `${this.items.length} resource(s) \u00b7 extensions need a reload after a change`),
192
+ width,
193
+ "",
194
+ );
174
195
  const lines = [line1, line2];
175
196
  if (this.searchActive) lines.push(...this.searchInput.render(width));
176
197
  lines.push("");
@@ -197,25 +218,42 @@ export class ConfigTab implements Component {
197
218
  handleInput(raw: string): void {
198
219
  if (this.busy) return;
199
220
  if (this.searchActive) {
200
- if (raw === "\x1b") { this.searchActive = false; this.searchInput.setValue?.(""); this.applyFilter(); }
201
- else if (raw === "\r") this.searchActive = false;
202
- else { this.searchInput.handleInput(raw); this.applyFilter(); }
221
+ if (raw === "\x1b") {
222
+ this.searchActive = false;
223
+ this.searchInput.setValue?.("");
224
+ this.applyFilter();
225
+ } else if (raw === "\r") this.searchActive = false;
226
+ else {
227
+ this.searchInput.handleInput(raw);
228
+ this.applyFilter();
229
+ }
203
230
  this.host.requestRender();
204
231
  return;
205
232
  }
206
233
  switch (raw) {
207
- case "\x1b[A": this.selectedIndex = (this.selectedIndex - 1 + this.filtered.length) % Math.max(this.filtered.length, 1); break;
208
- case "\x1b[B": this.selectedIndex = (this.selectedIndex + 1) % Math.max(this.filtered.length, 1); break;
234
+ case "\x1b[A":
235
+ this.selectedIndex = (this.selectedIndex - 1 + this.filtered.length) % Math.max(this.filtered.length, 1);
236
+ break;
237
+ case "\x1b[B":
238
+ this.selectedIndex = (this.selectedIndex + 1) % Math.max(this.filtered.length, 1);
239
+ break;
209
240
  case "v": // Tab is now reserved globally for sweeping between menus (TabbedContainer)
210
241
  this.scope = this.scope === "global" ? "project" : "global";
211
242
  this.rebuildItems();
212
243
  this.applyFilter();
213
244
  break;
214
- case "/": this.searchActive = true; break;
215
- case "r": void this.refresh(); return;
245
+ case "/":
246
+ this.searchActive = true;
247
+ break;
248
+ case "r":
249
+ void this.refresh();
250
+ return;
216
251
  case " ":
217
- case "\r": void this.toggleSelected(); return;
218
- default: return;
252
+ case "\r":
253
+ void this.toggleSelected();
254
+ return;
255
+ default:
256
+ return;
219
257
  }
220
258
  this.host.requestRender();
221
259
  }
@@ -7,11 +7,14 @@
7
7
  * dialog from any custom overlay, confirmed live as the same "opens a
8
8
  * different TUI" complaint the other three tabs had.
9
9
  */
10
+
11
+ import type { SecuritySettings } from "@danypops/packed/protocol";
10
12
  import { rawKeyHint } from "@earendil-works/pi-coding-agent";
11
13
  import { truncateToWidth } from "@earendil-works/pi-tui";
12
14
  import type { Component } from "malevich-tui-components";
13
- import type { SecuritySettings } from "@danypops/packed/protocol";
15
+
14
16
  type MutationApproval = SecuritySettings["mutationApproval"];
17
+
15
18
  import type { Natives } from "./packed.ts";
16
19
  import type { TabHost } from "./tab-host.ts";
17
20
 
@@ -20,7 +23,10 @@ const OPTIONS: Array<{ value: MutationApproval; label: string }> = [
20
23
  { value: "never", label: "Never require mutation approval (unsafe opt-out)" },
21
24
  ];
22
25
 
23
- interface Theme { fg(color: string, s: string): string; bold(s: string): string; }
26
+ interface Theme {
27
+ fg(color: string, s: string): string;
28
+ bold(s: string): string;
29
+ }
24
30
 
25
31
  export class SettingsTab implements Component {
26
32
  private current: MutationApproval = "always";
@@ -30,6 +36,7 @@ export class SettingsTab implements Component {
30
36
  constructor(
31
37
  private readonly natives: Pick<Natives, "security" | "setMutationApproval">,
32
38
  private readonly host: TabHost,
39
+ // biome-ignore lint/correctness/noUnusedPrivateClassMembers: read via `const { theme } = this` in render(), which Biome's usage check doesn't trace
33
40
  private readonly theme: Theme,
34
41
  ) {}
35
42
 
@@ -2,7 +2,11 @@ import { resolve } from "node:path";
2
2
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
3
3
  import type { Natives } from "./packed.js";
4
4
 
5
- interface SetupCommandArgs { action: "plan" | "apply"; path: string; prune: boolean; }
5
+ interface SetupCommandArgs {
6
+ action: "plan" | "apply";
7
+ path: string;
8
+ prune: boolean;
9
+ }
6
10
 
7
11
  function parseSetupCommand(args: string, cwd: string): SetupCommandArgs | undefined {
8
12
  const parts = args.trim().split(/\s+/).filter(Boolean);
@@ -12,7 +16,11 @@ function parseSetupCommand(args: string, cwd: string): SetupCommandArgs | undefi
12
16
  return { action: parts[1], path: resolve(cwd, path), prune };
13
17
  }
14
18
 
15
- export async function handleSetupCommand(args: string, ctx: ExtensionCommandContext, natives: Pick<Natives, "setupPlan" | "setupApply">): Promise<boolean> {
19
+ export async function handleSetupCommand(
20
+ args: string,
21
+ ctx: ExtensionCommandContext,
22
+ natives: Pick<Natives, "setupPlan" | "setupApply">,
23
+ ): Promise<boolean> {
16
24
  const parsed = parseSetupCommand(args, ctx.cwd);
17
25
  if (!parsed) return false;
18
26
  if (!ctx.hasUI) return true;