@gavin-hu/my-pi-agent 0.12.0 → 0.13.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.
Files changed (153) hide show
  1. package/README.md +28 -21
  2. package/README.zh-CN.md +30 -22
  3. package/extensions/bytefly/README.md +139 -67
  4. package/extensions/bytefly/chrome.ts +7 -2
  5. package/extensions/bytefly/doctor/register.ts +6 -0
  6. package/extensions/bytefly/doctor/runtime.ts +36 -6
  7. package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
  8. package/extensions/bytefly/footer/config.ts +7 -1
  9. package/extensions/bytefly/footer/delegation.ts +100 -0
  10. package/extensions/bytefly/footer/register.ts +13 -2
  11. package/extensions/bytefly/footer/render/lines.ts +2 -0
  12. package/extensions/bytefly/footer/render/segments.ts +27 -0
  13. package/extensions/bytefly/footer/types.ts +11 -0
  14. package/extensions/bytefly/index.ts +4 -3
  15. package/extensions/bytefly/notify/config.ts +2 -0
  16. package/extensions/bytefly/notify/format.ts +19 -1
  17. package/extensions/bytefly/notify/register.ts +37 -13
  18. package/extensions/bytefly/notify/types.ts +2 -0
  19. package/extensions/bytefly/panel/commands.ts +34 -13
  20. package/extensions/bytefly/panel/settings.ts +98 -16
  21. package/extensions/bytefly/update/cache.ts +73 -0
  22. package/extensions/bytefly/update/format.ts +65 -10
  23. package/extensions/bytefly/update/register.ts +99 -13
  24. package/extensions/bytefly/update/state.ts +13 -1
  25. package/extensions/document/README.md +22 -12
  26. package/extensions/document/config.ts +4 -0
  27. package/extensions/document/deadline.ts +61 -0
  28. package/extensions/document/tool.ts +13 -2
  29. package/extensions/file-browse/README.md +12 -8
  30. package/extensions/file-browse/client.ts +6 -7
  31. package/extensions/file-browse/files.ts +0 -4
  32. package/extensions/file-browse/git.ts +9 -10
  33. package/extensions/file-browse/html.ts +17 -7
  34. package/extensions/file-browse/router.ts +24 -15
  35. package/extensions/goal/README.md +7 -1
  36. package/extensions/goal/schema.ts +6 -2
  37. package/extensions/goal/types.ts +6 -2
  38. package/extensions/job/README.md +16 -12
  39. package/extensions/job/runtime.ts +23 -6
  40. package/extensions/job/status.ts +4 -2
  41. package/extensions/memory/README.md +55 -13
  42. package/extensions/memory/commands.ts +20 -2
  43. package/extensions/memory/doctor.ts +137 -0
  44. package/extensions/memory/format.ts +24 -7
  45. package/extensions/memory/runtime.ts +49 -0
  46. package/extensions/memory/store.ts +31 -0
  47. package/extensions/memory/tui.ts +136 -12
  48. package/extensions/plan/README.md +60 -29
  49. package/extensions/plan/commands.ts +67 -8
  50. package/extensions/plan/index.ts +35 -11
  51. package/extensions/plan/plan-index.ts +234 -0
  52. package/extensions/plan/plan-text.ts +95 -0
  53. package/extensions/plan/plans.ts +90 -1
  54. package/extensions/plan/review.ts +12 -9
  55. package/extensions/plan/runtime.ts +72 -27
  56. package/extensions/plan/steps.ts +35 -25
  57. package/extensions/plan/todos.ts +24 -2
  58. package/extensions/plan/tools.ts +124 -24
  59. package/extensions/plan/tui.ts +7 -28
  60. package/extensions/plan/types.ts +9 -0
  61. package/extensions/plan/web-page.ts +2 -2
  62. package/extensions/plan/web.ts +12 -11
  63. package/extensions/rewind/README.md +28 -8
  64. package/extensions/rewind/commands.ts +1 -1
  65. package/extensions/rewind/format.ts +4 -4
  66. package/extensions/rewind/git.ts +56 -18
  67. package/extensions/rewind/index.ts +3 -5
  68. package/extensions/rewind/path-picker.ts +198 -0
  69. package/extensions/rewind/paths.ts +128 -0
  70. package/extensions/rewind/restore.ts +69 -28
  71. package/extensions/rewind/rewind.ts +80 -3
  72. package/extensions/rewind/runtime.ts +13 -5
  73. package/extensions/rewind/snapshot-run.ts +5 -2
  74. package/extensions/rewind/types.ts +2 -0
  75. package/extensions/subagent/README.md +7 -1
  76. package/extensions/subagent/accounting.ts +34 -0
  77. package/extensions/subagent/index.ts +23 -1
  78. package/extensions/subagent/stream.ts +2 -0
  79. package/extensions/subagent/types.ts +2 -0
  80. package/extensions/watch/README.md +8 -3
  81. package/extensions/watch/index.ts +19 -0
  82. package/extensions/web-access/README.md +43 -13
  83. package/extensions/web-access/charset.ts +58 -0
  84. package/extensions/web-access/fetch/find.ts +11 -7
  85. package/extensions/web-access/fetch/format.ts +64 -18
  86. package/extensions/web-access/fetch/page.ts +5 -1
  87. package/extensions/web-access/fetch/tool.ts +19 -18
  88. package/extensions/web-access/http.ts +49 -8
  89. package/extensions/web-access/search/facts.ts +38 -0
  90. package/extensions/web-access/search/tool.ts +13 -0
  91. package/extensions/web-access/text.ts +50 -11
  92. package/extensions/worktree/README.md +74 -15
  93. package/extensions/worktree/config.ts +11 -0
  94. package/extensions/worktree/exit.ts +35 -3
  95. package/extensions/worktree/gaps.ts +74 -0
  96. package/extensions/worktree/guard.ts +50 -4
  97. package/extensions/worktree/handoff.ts +55 -0
  98. package/extensions/worktree/index.ts +12 -2
  99. package/extensions/worktree/runtime.ts +20 -0
  100. package/extensions/worktree/status.ts +15 -2
  101. package/extensions/worktree/tools.ts +3 -2
  102. package/lib/README.md +10 -8
  103. package/lib/events.ts +86 -1
  104. package/lib/goal-state.ts +95 -0
  105. package/lib/requirements.ts +282 -62
  106. package/lib/tui/README.md +91 -0
  107. package/lib/tui/glyphs.ts +1 -0
  108. package/lib/web/layout.ts +12 -2
  109. package/package.json +5 -3
  110. package/themes/bytefly-dark.json +1 -1
  111. package/assets/chrome.png +0 -0
  112. package/assets/file-browse.png +0 -0
  113. package/assets/job.png +0 -0
  114. package/assets/plan-review.png +0 -0
  115. package/assets/question.png +0 -0
  116. package/assets/rewind.png +0 -0
  117. package/assets/todo.png +0 -0
  118. package/assets/tool-renderings.png +0 -0
  119. package/assets/tools/ask_user_question-cancelled.png +0 -0
  120. package/assets/tools/ask_user_question.png +0 -0
  121. package/assets/tools/bash.png +0 -0
  122. package/assets/tools/edit.png +0 -0
  123. package/assets/tools/enter_plan_mode.png +0 -0
  124. package/assets/tools/enter_worktree.png +0 -0
  125. package/assets/tools/exit_plan_mode.png +0 -0
  126. package/assets/tools/exit_worktree.png +0 -0
  127. package/assets/tools/find.png +0 -0
  128. package/assets/tools/goal.png +0 -0
  129. package/assets/tools/grep.png +0 -0
  130. package/assets/tools/job.png +0 -0
  131. package/assets/tools/list_worktrees.png +0 -0
  132. package/assets/tools/ls.png +0 -0
  133. package/assets/tools/memory-long-subject.png +0 -0
  134. package/assets/tools/memory.png +0 -0
  135. package/assets/tools/notice.png +0 -0
  136. package/assets/tools/open_path.png +0 -0
  137. package/assets/tools/prune_worktrees.png +0 -0
  138. package/assets/tools/read.png +0 -0
  139. package/assets/tools/read_doc-running.png +0 -0
  140. package/assets/tools/read_doc.png +0 -0
  141. package/assets/tools/run_doctor.png +0 -0
  142. package/assets/tools/send_wechat.png +0 -0
  143. package/assets/tools/set_plan_steps.png +0 -0
  144. package/assets/tools/subagent.png +0 -0
  145. package/assets/tools/todo.png +0 -0
  146. package/assets/tools/turn-rule.png +0 -0
  147. package/assets/tools/user-message.png +0 -0
  148. package/assets/tools/watch.png +0 -0
  149. package/assets/tools/web_fetch-rejected.png +0 -0
  150. package/assets/tools/web_fetch.png +0 -0
  151. package/assets/tools/web_search.png +0 -0
  152. package/assets/tools/write.png +0 -0
  153. package/assets/tools/write_plan.png +0 -0
@@ -15,7 +15,8 @@
15
15
  */
16
16
 
17
17
  import { join } from "node:path";
18
- import { type JsonReadResult, SHARED_CONFIG_FILE } from "./config.ts";
18
+ import { configSection, configValueAt, type JsonReadResult, SHARED_CONFIG_FILE } from "./config.ts";
19
+ import type { WebSearchProviderEvent } from "./events.ts";
19
20
  import { getPiInvocation, type PiInvocation } from "./pi-invocation.ts";
20
21
 
21
22
  /** How a check came out. Only `missing` is worth interrupting a session for. */
@@ -73,7 +74,51 @@ export interface NotifyCheck {
73
74
  hint: string;
74
75
  }
75
76
 
76
- export type RequirementCheck = BinaryCheck | ModuleCheck | StateCheck | InvocationCheck | NotifyCheck;
77
+ /**
78
+ * The resolved `web_search` provider, as `web-access` publishes it.
79
+ *
80
+ * The provider list, each provider's configuration test, and the `auto` rule live
81
+ * in `web-access`, so the check reads a published summary rather than rebuilding
82
+ * any of it here; the payload never carries the key itself.
83
+ */
84
+ export interface ProviderCheck {
85
+ kind: "provider";
86
+ enables: string;
87
+ hint: string;
88
+ }
89
+
90
+ /**
91
+ * A config switch whose "off" value trades a safety net away.
92
+ *
93
+ * A prerequisite table can already say "this is missing"; this says "you turned
94
+ * this protection off", which is the other way an extension ends up degraded
95
+ * without anything being absent. The value is read from `bytefly.json` (global,
96
+ * then project, project wins), so the check needs no extension to publish
97
+ * anything — the switch is the whole fact.
98
+ */
99
+ export interface SafetyCheck {
100
+ kind: "safety";
101
+ /** The `bytefly.json` section that owns the switch. */
102
+ section: string;
103
+ /** The key path inside that section. */
104
+ keyPath: readonly string[];
105
+ /** The value that means the net is off. */
106
+ off: boolean;
107
+ /** What the switch protects, for the passing case. */
108
+ enables: string;
109
+ /** What is true once it is off, for the degraded case. */
110
+ disabled: string;
111
+ hint: string;
112
+ }
113
+
114
+ export type RequirementCheck =
115
+ | BinaryCheck
116
+ | ModuleCheck
117
+ | StateCheck
118
+ | InvocationCheck
119
+ | NotifyCheck
120
+ | ProviderCheck
121
+ | SafetyCheck;
77
122
 
78
123
  /** What an extension needs, split by how badly it needs it. */
79
124
  export interface ExtensionRequirements {
@@ -138,7 +183,22 @@ function gitCheck(enables: string): BinaryCheck {
138
183
  export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
139
184
  worktree: {
140
185
  requires: [gitCheck("worktree isolation")],
141
- notes: ["`.worktreeinclude` symlinks need symlink support (Windows Developer Mode)."],
186
+ optional: [
187
+ {
188
+ kind: "safety",
189
+ section: "worktree",
190
+ keyPath: ["guard", "blockPowershell"],
191
+ off: false,
192
+ enables: "`powershell` is refused while the session is isolated",
193
+ disabled: "`powershell` runs unguarded, with its relative paths against the main checkout",
194
+ hint: "set `worktree.guard.blockPowershell` back to true, or use `bash`, which is re-rooted and guarded.",
195
+ },
196
+ ],
197
+ notes: [
198
+ "`.worktreeinclude` symlinks need symlink support (Windows Developer Mode).",
199
+ "Your own `!` shell is not re-rooted or guarded; only the model's own tools are.",
200
+ "Commands whose git behavior cannot be verified pass through unless `worktree.guard.blockUnparsableCommands` is set.",
201
+ ],
142
202
  },
143
203
  rewind: {
144
204
  requires: [gitCheck("rewind snapshots")],
@@ -164,7 +224,7 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
164
224
  },
165
225
  job: {
166
226
  notes: [
167
- "Windows: no POSIX `EXIT` trap, so a reattached job's status stays `unknown`.",
227
+ "Windows: a job cannot be detached (`detached: true` is refused and `job.detachedByDefault` is inert), and a job reported only after its process is gone stays `unknown` — there is no shell `EXIT` trap.",
168
228
  "`ps` is used for the pid-reuse guard and is POSIX-only.",
169
229
  ],
170
230
  },
@@ -175,7 +235,14 @@ export const REQUIREMENTS: Readonly<Record<string, ExtensionRequirements>> = {
175
235
  ],
176
236
  },
177
237
  "web-access": {
178
- optional: [{ kind: "module", name: "unpdf", enables: "PDF text extraction in `web_fetch`", hint: pdfGuidance() }],
238
+ optional: [
239
+ { kind: "module", name: "unpdf", enables: "PDF text extraction in `web_fetch`", hint: pdfGuidance() },
240
+ {
241
+ kind: "provider",
242
+ enables: "`web_search` through the configured provider",
243
+ hint: "set `web-access.search.provider` and its `endpoint`/`apiKeyEnv`/`apiKey` in bytefly.json, or export the key variable.",
244
+ },
245
+ ],
179
246
  },
180
247
  "file-browse": {
181
248
  optional: [gitCheck("the branch chip and the diff view")],
@@ -253,6 +320,14 @@ export interface ProbeSeams {
253
320
  notify?: () => { enabled: boolean; method: string } | undefined;
254
321
  /** Whether stdout is a terminal, which the OSC methods need; defaults to true. */
255
322
  isTTY?: boolean;
323
+ /** The search provider `web-access` resolved for this session, when it published one. */
324
+ searchProvider?: () => WebSearchProviderEvent | undefined;
325
+ /** Read an environment variable; defaults to the process environment. */
326
+ env?: (name: string) => string | undefined;
327
+ /** Probe a URL's reachability; only called when `refresh` is true. */
328
+ reach?: (url: string) => Promise<{ ok: boolean; error?: string }>;
329
+ /** True when the caller asked for an explicit refresh, which is when a probe may touch the network. */
330
+ refresh?: boolean;
256
331
  }
257
332
 
258
333
  /**
@@ -292,13 +367,140 @@ async function probeCode(invocation: PiInvocation, seams: ProbeSeams): Promise<n
292
367
  }
293
368
  }
294
369
 
370
+ /**
371
+ * The findings for the `web_search` provider check.
372
+ *
373
+ * Up to three separate facts, because they have three different fixes: which
374
+ * provider is in use, whether its key variable is set, and — only on an explicit
375
+ * refresh — whether its configurable endpoint answers.
376
+ */
377
+ async function providerFindings(
378
+ extension: string,
379
+ check: ProviderCheck,
380
+ failSeverity: FindingSeverity,
381
+ seams: ProbeSeams,
382
+ ): Promise<Finding[]> {
383
+ const facts = seams.searchProvider?.();
384
+ if (!facts) {
385
+ return [
386
+ {
387
+ extension,
388
+ check: "web-access.search.provider",
389
+ severity: "info",
390
+ detail: "web-access has not reported a search provider in this session",
391
+ },
392
+ ];
393
+ }
394
+
395
+ const findings: Finding[] = [];
396
+ findings.push({
397
+ extension,
398
+ check: "web-access.search.provider",
399
+ severity: facts.configured ? "ok" : failSeverity,
400
+ detail: facts.configured
401
+ ? `resolved to ${facts.provider} (${facts.reason})`
402
+ : `resolved to ${facts.provider} (${facts.reason}), which is not configured — ${check.enables}`,
403
+ ...(!facts.configured ? { hint: check.hint } : {}),
404
+ });
405
+
406
+ const env = seams.env ?? ((name: string) => process.env[name]);
407
+ if (facts.apiKeyEnv && !facts.keyConfigured) {
408
+ const present = (env(facts.apiKeyEnv) ?? "").trim() !== "";
409
+ findings.push({
410
+ extension,
411
+ check: facts.apiKeyEnv,
412
+ severity: present ? "ok" : failSeverity,
413
+ detail: present
414
+ ? `${facts.apiKeyEnv} is set — the ${facts.provider} API key`
415
+ : `the API key for ${facts.provider} is missing — ${facts.apiKeyEnv} is not set`,
416
+ ...(present ? {} : { hint: `export ${facts.apiKeyEnv}, or set web-access.search.apiKey in bytefly.json.` }),
417
+ });
418
+ }
419
+
420
+ // Reachability is the one probe that touches the network, so it runs only when
421
+ // the caller asked for an explicit refresh.
422
+ if (seams.refresh === true && facts.endpoint !== null) {
423
+ const reach = seams.reach;
424
+ if (!reach) {
425
+ findings.push({
426
+ extension,
427
+ check: facts.endpoint,
428
+ severity: "info",
429
+ detail: "no reachability probe is available in this context",
430
+ });
431
+ } else {
432
+ const outcome = await reach(facts.endpoint).catch((error: unknown) => ({
433
+ ok: false,
434
+ error: error instanceof Error ? error.message : String(error),
435
+ }));
436
+ findings.push({
437
+ extension,
438
+ check: facts.endpoint,
439
+ severity: outcome.ok ? "ok" : failSeverity,
440
+ detail: outcome.ok
441
+ ? `${facts.endpoint} answered`
442
+ : `${facts.endpoint} did not answer (${outcome.error ?? "no response"})`,
443
+ ...(outcome.ok
444
+ ? {}
445
+ : { hint: "check web-access.search.endpoint in bytefly.json, or that the SearXNG server is running." }),
446
+ });
447
+ }
448
+ }
449
+
450
+ return findings;
451
+ }
452
+
453
+ /**
454
+ * The effective value of one config leaf, global then project. An absent or
455
+ * unparseable document contributes nothing — the malformed-JSON check owns that
456
+ * case — so this returns `undefined` rather than a guess.
457
+ */
458
+ function configLeaf(check: SafetyCheck, seams: ProbeSeams): { value: unknown; path: string } | undefined {
459
+ const candidates = [
460
+ join(seams.dirs.agentDir, SHARED_CONFIG_FILE),
461
+ join(seams.dirs.cwd, seams.dirs.configDirName, SHARED_CONFIG_FILE),
462
+ ];
463
+ let found: { value: unknown; path: string } | undefined;
464
+ for (const path of candidates) {
465
+ const result = seams.readJson(path);
466
+ if (result.state !== "ok") continue;
467
+ const section = configSection(result.value, check.section);
468
+ const value = configValueAt(section, check.keyPath);
469
+ if (value !== undefined) found = { value, path };
470
+ }
471
+ return found;
472
+ }
473
+
474
+ /** The findings for a switch whose off value trades a safety net away. */
475
+ function safetyFindings(
476
+ extension: string,
477
+ check: SafetyCheck,
478
+ failSeverity: FindingSeverity,
479
+ seams: ProbeSeams,
480
+ ): Finding[] {
481
+ const leaf = configLeaf(check, seams);
482
+ const label = [check.section, ...check.keyPath].join(".");
483
+ if (leaf?.value === check.off) {
484
+ return [
485
+ {
486
+ extension,
487
+ check: label,
488
+ severity: failSeverity,
489
+ detail: `is ${String(check.off)} in ${leaf.path} — ${check.disabled}`,
490
+ hint: check.hint,
491
+ },
492
+ ];
493
+ }
494
+ return [{ extension, check: label, severity: "ok", detail: check.enables }];
495
+ }
496
+
295
497
  /** Run one check. `failSeverity` comes from which list the check sits in. */
296
498
  async function runCheck(
297
499
  extension: string,
298
500
  check: RequirementCheck,
299
501
  failSeverity: FindingSeverity,
300
502
  seams: ProbeSeams,
301
- ): Promise<Finding> {
503
+ ): Promise<Finding[]> {
302
504
  switch (check.kind) {
303
505
  case "binary": {
304
506
  const invocation = check.invocation?.() ?? {
@@ -306,91 +508,109 @@ async function runCheck(
306
508
  args: [...(check.probeArgs ?? ["--version"])],
307
509
  };
308
510
  const code = await probeCode(invocation, seams);
309
- return code === 0
310
- ? { extension, check: check.name, severity: "ok", detail: check.enables }
311
- : {
312
- extension,
313
- check: check.name,
314
- severity: failSeverity,
315
- detail: `not found on PATH — ${check.enables}`,
316
- hint: check.hint,
317
- };
511
+ return [
512
+ code === 0
513
+ ? { extension, check: check.name, severity: "ok", detail: check.enables }
514
+ : {
515
+ extension,
516
+ check: check.name,
517
+ severity: failSeverity,
518
+ detail: `not found on PATH — ${check.enables}`,
519
+ hint: check.hint,
520
+ },
521
+ ];
318
522
  }
319
523
  case "module": {
320
524
  const load = seams.loadModule ?? defaultLoadModule;
321
525
  try {
322
526
  await load(check.name);
323
527
  } catch {
324
- return {
325
- extension,
326
- check: check.name,
327
- severity: failSeverity,
328
- detail: `not installed — ${check.enables}`,
329
- hint: check.hint,
330
- };
528
+ return [
529
+ {
530
+ extension,
531
+ check: check.name,
532
+ severity: failSeverity,
533
+ detail: `not installed — ${check.enables}`,
534
+ hint: check.hint,
535
+ },
536
+ ];
331
537
  }
332
- return { extension, check: check.name, severity: "ok", detail: check.enables };
538
+ return [{ extension, check: check.name, severity: "ok", detail: check.enables }];
333
539
  }
334
540
  case "state": {
335
541
  const path = join(scopeDir(check.scope, seams.dirs), check.file);
336
542
  const result = seams.readJson(path);
337
- if (result.state === "ok") return { extension, check: path, severity: "ok", detail: check.enables };
543
+ if (result.state === "ok") return [{ extension, check: path, severity: "ok", detail: check.enables }];
338
544
  if (result.state === "absent") {
339
- return {
545
+ return [
546
+ {
547
+ extension,
548
+ check: path,
549
+ severity: failSeverity,
550
+ detail: `missing — ${check.enables}`,
551
+ hint: check.hint,
552
+ },
553
+ ];
554
+ }
555
+ return [
556
+ {
340
557
  extension,
341
558
  check: path,
342
559
  severity: failSeverity,
343
- detail: `missing — ${check.enables}`,
560
+ detail: `unreadable (${result.error}) — ${check.enables}`,
344
561
  hint: check.hint,
345
- };
346
- }
347
- return {
348
- extension,
349
- check: path,
350
- severity: failSeverity,
351
- detail: `unreadable (${result.error}) — ${check.enables}`,
352
- hint: check.hint,
353
- };
562
+ },
563
+ ];
354
564
  }
355
565
  case "invocation": {
356
566
  const invocation = seams.invocation?.() ?? getPiInvocation(["--version"]);
357
567
  // Reusing the running interpreter cannot fail: the process is running.
358
568
  const code = invocation.command === process.execPath ? 0 : await probeCode(invocation, seams);
359
- return code === 0
360
- ? { extension, check: invocation.command, severity: "ok", detail: check.enables }
361
- : {
362
- extension,
363
- check: invocation.command,
364
- severity: failSeverity,
365
- detail: `cannot be run — ${check.enables}`,
366
- hint: check.hint,
367
- };
569
+ return [
570
+ code === 0
571
+ ? { extension, check: invocation.command, severity: "ok", detail: check.enables }
572
+ : {
573
+ extension,
574
+ check: invocation.command,
575
+ severity: failSeverity,
576
+ detail: `cannot be run — ${check.enables}`,
577
+ hint: check.hint,
578
+ },
579
+ ];
368
580
  }
369
581
  case "notify": {
370
582
  const notify = seams.notify?.();
371
583
  if (!notify?.enabled) {
372
- return { extension, check: "notify.method", severity: "ok", detail: "notifications are off" };
584
+ return [{ extension, check: "notify.method", severity: "ok", detail: "notifications are off" }];
373
585
  }
374
586
  const env = { platform: seams.platform, isTTY: seams.isTTY ?? true };
375
587
  if (canDeliver(notify.method, env)) {
376
- return {
588
+ return [
589
+ {
590
+ extension,
591
+ check: "notify.method",
592
+ severity: "ok",
593
+ detail:
594
+ notify.method === "auto"
595
+ ? "auto picks a route from the environment"
596
+ : `"${notify.method}" delivers on ${seams.platform}`,
597
+ },
598
+ ];
599
+ }
600
+ return [
601
+ {
377
602
  extension,
378
603
  check: "notify.method",
379
- severity: "ok",
380
- detail:
381
- notify.method === "auto"
382
- ? "auto picks a route from the environment"
383
- : `"${notify.method}" delivers on ${seams.platform}`,
384
- };
385
- }
386
- return {
387
- extension,
388
- check: "notify.method",
389
- severity: failSeverity,
390
- detail: `"${notify.method}" cannot deliver on ${seams.platform} — ${check.enables}`,
391
- hint: check.hint,
392
- };
604
+ severity: failSeverity,
605
+ detail: `"${notify.method}" cannot deliver on ${seams.platform} — ${check.enables}`,
606
+ hint: check.hint,
607
+ },
608
+ ];
393
609
  }
610
+ case "provider":
611
+ return providerFindings(extension, check, failSeverity, seams);
612
+ case "safety":
613
+ return safetyFindings(extension, check, failSeverity, seams);
394
614
  }
395
615
  }
396
616
 
@@ -461,8 +681,8 @@ export async function probeExtension(extension: string, seams: ProbeSeams): Prom
461
681
  }
462
682
 
463
683
  const findings: Finding[] = [];
464
- for (const check of spec.requires ?? []) findings.push(await runCheck(extension, check, "missing", seams));
465
- for (const check of spec.optional ?? []) findings.push(await runCheck(extension, check, "degraded", seams));
684
+ for (const check of spec.requires ?? []) findings.push(...(await runCheck(extension, check, "missing", seams)));
685
+ for (const check of spec.optional ?? []) findings.push(...(await runCheck(extension, check, "degraded", seams)));
466
686
  findings.push(...configFindings(extension, spec, seams));
467
687
  for (const note of spec.notes ?? []) {
468
688
  findings.push({ extension, check: "platform", severity: "info", detail: note });
@@ -0,0 +1,91 @@
1
+ # lib/tui — the shared terminal design system
2
+
3
+ The primitives every interactive surface in this package draws with: rules,
4
+ rows, columns, gauges, panes, glyphs, color roles, and the tool-transcript kit.
5
+ The module-by-module inventory is the `tui/` row of [`../README.md`](../README.md);
6
+ this file is the conventions those modules exist to make easy.
7
+
8
+ Pi's [Terminal UI](https://github.com/earendil-works/pi/tree/main/packages/coding-agent/docs/tui.md)
9
+ and [Themes](https://github.com/earendil-works/pi/tree/main/packages/coding-agent/docs/themes.md)
10
+ docs are normative. Follow Pi's built-in design system and do not invent a
11
+ private visual language; a surface that re-derives chrome, glyphs, or scroll math
12
+ instead of importing it is the bug this directory exists to prevent.
13
+
14
+ ## Host before custom code
15
+
16
+ Reach down the ladder in order and stop at the first rung that works:
17
+ `ctx.ui.select` / `confirm` / `input` / `editor`, then `notify` / `setStatus` /
18
+ `setWidget`, then the pi-tui components. Reach for `ctx.ui.custom()` or a
19
+ tool/entry renderer only when a surface needs its own layout or input, and never
20
+ build a second terminal renderer.
21
+
22
+ ## Block grammar
23
+
24
+ - Match the built-in block grammar: one leading blank line per transcript block,
25
+ nothing trailing (the host owns spacing).
26
+ - Frame screens with a plain `─` rule, not box corners.
27
+ - Compact by default, expand for detail: label-first one-line widgets, with more
28
+ revealed on demand or on a dedicated screen.
29
+
30
+ ## Primitives
31
+
32
+ Reach for a `lib/tui/` primitive before composing your own: `plainRule` /
33
+ `titleRule` for a heading, `listRow` / `checkbox` for a list, `gaugeBar` for a
34
+ meter, `emptyState` for an empty screen, `splitRow` / `fitCells` for two columns,
35
+ `LinePane` for a scrollable log, and `ListCursor` / `IdCursor` for selection.
36
+
37
+ ## Color is a role
38
+
39
+ Resolve colors through a [`tokens.ts`](./tokens.ts) role
40
+ (`paint(theme, role, text)`), never a literal ANSI sequence, hex value, or Pi
41
+ token named inline; a theme swap must re-skin every surface. Use the shared
42
+ `selectionMarker`, `GLYPHS`, `SEPARATORS`, and `STATUS_KEYS` instead of retyping
43
+ their literals, and `screenHint` / Pi's `keyHint` for the hint row.
44
+
45
+ ## Width is columns
46
+
47
+ Width is terminal columns, not string length. Use `visibleWidth`,
48
+ `truncateToWidth`, and `wrapTextWithAnsi`; every returned line must fit the
49
+ width, and width arithmetic starts from `Math.max(1, width)`. Size from the live
50
+ viewport — `titleRule`, `viewportRows`, `fitRows`, and `ListCursor`, fed by a
51
+ `ViewportRowsSource` (number or getter) with a sensible fallback — so a resize is
52
+ picked up. List screens stay docked; only the plan review uses
53
+ `FULL_SCREEN_OVERLAY`.
54
+
55
+ ## Sanitize at the boundary
56
+
57
+ Run model- or file-authored text through `sanitizeLine` before it reaches a
58
+ widget or the terminal: it neutralizes control characters and then collapses the
59
+ text to one line. `stripControlChars` alone is for multi-line text that must keep
60
+ its newlines.
61
+
62
+ ## Keyboard first
63
+
64
+ Use `matchesKey` / `Key`, give every mouse interaction a keyboard path, and keep
65
+ the shared bindings: Esc closes, `j` / `k` and arrows scroll, `g` / `G` and
66
+ Home/End jump.
67
+
68
+ ## Components are derived and cheap
69
+
70
+ Implement `Component` with `invalidate()`, update an instance in place, read live
71
+ state on each render, and call `requestRender()` only after state changes. Never
72
+ hold themed strings in state unless `invalidate()` rebuilds them. A widget is
73
+ derived from state, never authoritative.
74
+
75
+ Pi renders the whole tree every frame, so a component that derives its lines from
76
+ another one memoizes them with `RenderCache` (keyed on width and the source
77
+ lines' identity) and drops the memo in `invalidate()`. Re-deriving every line per
78
+ frame is what makes a long transcript feel slow.
79
+
80
+ ## Focus
81
+
82
+ Reserve the name `focused` for a real focus `get` / `set` pair. Pi's `setFocus`
83
+ duck-types a focusable with `"focused" in component` and then assigns a boolean
84
+ there, so any other member of that name — a method, say — is silently shadowed
85
+ and the first call throws at runtime.
86
+
87
+ ## Tests
88
+
89
+ A module here gets a sibling `<module>.test.ts` covering fields, width bounds,
90
+ ordering, and glyphs — never literal column padding. An extension's screen test
91
+ is the `tui.test.ts` beside its `tui.ts`.
package/lib/tui/glyphs.ts CHANGED
@@ -30,6 +30,7 @@ export const GLYPHS = {
30
30
  jobsFailure: "✗",
31
31
  stopped: "⊘",
32
32
  browse: "◉",
33
+ delegation: "↳",
33
34
  wechat: "✆",
34
35
  gaugeFull: "▰",
35
36
  gaugeEmpty: "▱",
package/lib/web/layout.ts CHANGED
@@ -14,15 +14,25 @@
14
14
  *
15
15
  * The shell is a definite full-height column so the panes scroll inside it; the
16
16
  * 860px breakpoint drops that back to normal page scroll for narrow screens.
17
+ *
18
+ * The sidebar scrolls on its own box rather than on the `.bf-panel` it holds:
19
+ * a `<details>` lays its content out in a block wrapper with no definite
20
+ * height that takes no part in the element's flex layout, so a flex child there
21
+ * keeps its content height and the sidebar would only clip it. A page that
22
+ * needs pinned chrome above its own scroll pane (file-browse) fills that
23
+ * wrapper absolutely and scrolls an inner region instead.
17
24
  */
18
25
 
26
+ /** Viewport width, in px, at and below which the sidebar becomes a disclosure. */
27
+ export const SIDEBAR_COLLAPSE_MAX = 860;
28
+
19
29
  export const WEB_LAYOUT_CSS = `.bf-app{display:flex;flex-direction:column;height:100vh;min-height:0;overflow:hidden;background:var(--canvas);color:var(--text);font:var(--fs-base)/var(--lh-normal) var(--font-ui)}
20
30
  .bf-app__header{position:sticky;top:0;z-index:2;flex:0 0 auto;background:var(--surface);border-bottom:1px solid var(--border)}
21
31
  .bf-app__body{flex:1 1 auto;min-height:0;display:grid;grid-template-columns:260px minmax(0,1fr)}
22
- .bf-app__sidebar{display:flex;flex-direction:column;min-width:0;min-height:0;overflow:hidden;background:var(--surface);border-right:1px solid var(--border)}
32
+ .bf-app__sidebar{display:flex;flex-direction:column;min-width:0;min-height:0;overflow-x:hidden;overflow-y:auto;background:var(--surface);border-right:1px solid var(--border)}
23
33
  .bf-app__main{min-width:0;min-height:0;display:flex;flex-direction:column;overflow:auto;padding:var(--space-5) var(--space-6)}
24
34
  .bf-app__footer{flex:0 0 auto;border-top:1px solid var(--border);color:var(--muted);font-size:var(--fs-sm);padding:var(--space-3) var(--space-6)}
25
- @media (max-width:860px){
35
+ @media (max-width:${SIDEBAR_COLLAPSE_MAX}px){
26
36
  .bf-app{height:auto;min-height:100vh;overflow:visible}
27
37
  .bf-app__body{flex:none;grid-template-columns:1fr}
28
38
  .bf-app__sidebar{display:block;position:static;max-height:none;overflow:visible;border-right:0;border-bottom:1px solid var(--border)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gavin-hu/my-pi-agent",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Personal Pi customizations, packaged as a Pi package: worktree isolation and rewind snapshots, question, todo, goal, plan, subagent, job, watch, web-access, file-browse, file-open, document, memory, wechat, and bytefly.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -59,14 +59,14 @@
59
59
  "url": "https://github.com/gavin-hu/my-pi-agent.git"
60
60
  },
61
61
  "files": [
62
- "assets/**",
63
62
  "extensions/**",
64
63
  "lib/**",
65
64
  "themes/**",
66
65
  "!**/*.test.ts"
67
66
  ],
68
67
  "publishConfig": {
69
- "access": "public"
68
+ "access": "public",
69
+ "provenance": true
70
70
  },
71
71
  "pi": {
72
72
  "extensions": [
@@ -103,6 +103,8 @@
103
103
  "check:serial": "bun run format:check && bun run typecheck && bun run test:serial",
104
104
  "release:notes": "bun run scripts/release-notes.ts",
105
105
  "config:migrate": "bun run scripts/migrate-config.ts",
106
+ "compat:versions": "bun run scripts/peer-versions.ts",
107
+ "bench:render": "bun run scripts/render-bench.ts",
106
108
  "screenshot:plan": "bun run scripts/plan-review-shot.ts",
107
109
  "screenshot:tools": "bun run scripts/tool-renderings.ts gallery assets/tool-renderings.png",
108
110
  "screenshot:tool-pngs": "bun run scripts/tool-renderings.ts tools assets/tools",
@@ -9,7 +9,7 @@
9
9
  "borderMuted": "#5a626b",
10
10
  "text": "#c9d1d9",
11
11
  "muted": "#8b949e",
12
- "dim": "#6e7681",
12
+ "dim": "#7d8590",
13
13
  "blue": "#58a6ff",
14
14
  "green": "#3fb950",
15
15
  "red": "#f85149",
package/assets/chrome.png DELETED
Binary file
Binary file
package/assets/job.png DELETED
Binary file
Binary file
Binary file
package/assets/rewind.png DELETED
Binary file
package/assets/todo.png DELETED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file