pi-ui-extend 1.0.41 → 1.0.42

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 (124) hide show
  1. package/README.md +10 -2
  2. package/dist/app/app.d.ts +3 -0
  3. package/dist/app/app.js +26 -0
  4. package/dist/app/commands/command-host.d.ts +1 -0
  5. package/dist/app/commands/command-model-actions.js +6 -1
  6. package/dist/app/commands/command-registry.js +1 -1
  7. package/dist/app/commands/command-session-actions.js +9 -3
  8. package/dist/app/commands/reload-context-inventory.d.ts +11 -0
  9. package/dist/app/commands/reload-context-inventory.js +59 -0
  10. package/dist/app/extensions/extension-actions-controller.d.ts +1 -0
  11. package/dist/app/extensions/extension-actions-controller.js +6 -0
  12. package/dist/app/extensions/subagent-catalog-state.d.ts +9 -0
  13. package/dist/app/extensions/subagent-catalog-state.js +23 -0
  14. package/dist/app/rendering/editor-panels.js +10 -4
  15. package/dist/app/session/session-lifecycle-controller.d.ts +1 -0
  16. package/dist/app/session/session-lifecycle-controller.js +6 -0
  17. package/dist/app/session/tabs-controller.d.ts +1 -0
  18. package/dist/app/session/tabs-controller.js +19 -1
  19. package/dist/app/subagents/subagents-files.js +60 -1
  20. package/dist/app/subagents/subagents-model.d.ts +1 -0
  21. package/dist/app/subagents/subagents-model.js +18 -2
  22. package/dist/app/types.d.ts +5 -0
  23. package/dist/default-pix-config.js +10 -0
  24. package/dist/schemas/index.d.ts +1 -0
  25. package/dist/schemas/index.js +1 -0
  26. package/dist/schemas/pi-tools-suite-schema.d.ts +80 -58
  27. package/dist/schemas/pi-tools-suite-schema.js +43 -78
  28. package/dist/schemas/pix-schema.d.ts +7 -0
  29. package/dist/schemas/pix-schema.js +14 -0
  30. package/dist/schemas/tasks-schema.d.ts +18 -0
  31. package/dist/schemas/tasks-schema.js +41 -0
  32. package/docs/concurrency.md +6 -0
  33. package/docs/desktop-mvp.md +12 -1
  34. package/docs/desktop-task-manager.md +93 -82
  35. package/external/pi-tools-suite/README.md +37 -50
  36. package/external/pi-tools-suite/docs/browser-qa-subagent.md +19 -0
  37. package/external/pi-tools-suite/docs/evals.md +1 -1
  38. package/external/pi-tools-suite/docs/session-recovery.md +40 -14
  39. package/external/pi-tools-suite/docs/subagent-model-pools.md +38 -27
  40. package/external/pi-tools-suite/src/async-subagents/agents/frontier-review.md +23 -0
  41. package/external/pi-tools-suite/src/async-subagents/agents/presets.jsonc +16 -0
  42. package/external/pi-tools-suite/src/async-subagents/agents/research.md +5 -3
  43. package/external/pi-tools-suite/src/async-subagents/commands.ts +14 -70
  44. package/external/pi-tools-suite/src/async-subagents/core/activity.ts +33 -0
  45. package/external/pi-tools-suite/src/async-subagents/core/agent-catalog.ts +5 -4
  46. package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +1 -1
  47. package/external/pi-tools-suite/src/async-subagents/core/agents-dir.ts +8 -3
  48. package/external/pi-tools-suite/src/async-subagents/core/config.ts +122 -312
  49. package/external/pi-tools-suite/src/async-subagents/core/routing.ts +15 -2
  50. package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +6 -2
  51. package/external/pi-tools-suite/src/async-subagents/core/state.ts +40 -0
  52. package/external/pi-tools-suite/src/async-subagents/core/types.ts +7 -0
  53. package/external/pi-tools-suite/src/async-subagents/index.ts +69 -5
  54. package/external/pi-tools-suite/src/async-subagents/lib.ts +7 -8
  55. package/external/pi-tools-suite/src/async-subagents/tools/spawn.ts +8 -4
  56. package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +3 -2
  57. package/external/pi-tools-suite/src/async-subagents/types.ts +1 -0
  58. package/external/pi-tools-suite/src/config.ts +41 -1
  59. package/external/pi-tools-suite/src/context-gateway/config.ts +11 -2
  60. package/external/pi-tools-suite/src/context-gateway/enforcement.ts +102 -0
  61. package/external/pi-tools-suite/src/context-gateway/index.ts +73 -19
  62. package/external/pi-tools-suite/src/context-gateway/storeless-capabilities.ts +3 -3
  63. package/external/pi-tools-suite/src/context-gateway/telemetry.ts +101 -28
  64. package/external/pi-tools-suite/src/context-gateway/types.ts +9 -3
  65. package/external/pi-tools-suite/src/context-inventory.ts +99 -0
  66. package/external/pi-tools-suite/src/dcp/auto-compress.ts +96 -44
  67. package/external/pi-tools-suite/src/dcp/commands.ts +18 -0
  68. package/external/pi-tools-suite/src/dcp/compress-tool.ts +170 -45
  69. package/external/pi-tools-suite/src/dcp/compression-blocks.ts +6 -58
  70. package/external/pi-tools-suite/src/dcp/compression-preview.ts +9 -0
  71. package/external/pi-tools-suite/src/dcp/config.ts +56 -13
  72. package/external/pi-tools-suite/src/dcp/prompts.ts +83 -192
  73. package/external/pi-tools-suite/src/dcp/protected-continuity.ts +175 -0
  74. package/external/pi-tools-suite/src/dcp/pruner-candidates.ts +1 -1
  75. package/external/pi-tools-suite/src/dcp/pruner-message-ids.ts +6 -10
  76. package/external/pi-tools-suite/src/dcp/pruner-tools.ts +6 -3
  77. package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +13 -9
  78. package/external/pi-tools-suite/src/index.ts +7 -1
  79. package/external/pi-tools-suite/src/resource-registry/index.ts +2290 -0
  80. package/external/pi-tools-suite/src/shell-command-policy.ts +219 -0
  81. package/external/pi-tools-suite/src/tool-descriptions.ts +3 -4
  82. package/package.json +4 -2
  83. package/schemas/pi-tools-suite.json +427 -280
  84. package/schemas/pix.json +24 -0
  85. package/schemas/tasks.json +132 -0
  86. package/skills/spec-wiki/SKILL.md +55 -0
  87. package/skills/spec-wiki/evals/evals.json +132 -0
  88. package/skills/spec-wiki/evals/files/bootstrap/architecture/rfc-007.md +14 -0
  89. package/skills/spec-wiki/evals/files/bootstrap/docs/how-to-pay.md +4 -0
  90. package/skills/spec-wiki/evals/files/bootstrap/docs/overview.md +9 -0
  91. package/skills/spec-wiki/evals/files/bootstrap/requirements/payment-behavior.md +17 -0
  92. package/skills/spec-wiki/evals/files/bootstrap/src/payments.ts +12 -0
  93. package/skills/spec-wiki/evals/files/bootstrap/tests/payments.test.ts +11 -0
  94. package/skills/spec-wiki/evals/files/focused/src/session.ts +5 -0
  95. package/skills/spec-wiki/evals/files/focused/tests/session.test.ts +17 -0
  96. package/skills/spec-wiki/evals/files/maintenance/low-signal/incoming/capture.txt +2 -0
  97. package/skills/spec-wiki/evals/files/maintenance/low-signal/src/capture.ts +3 -0
  98. package/skills/spec-wiki/evals/files/maintenance/move/docs/old-job-contract.md +18 -0
  99. package/skills/spec-wiki/evals/files/maintenance/move/src/job.ts +5 -0
  100. package/skills/spec-wiki/evals/files/maintenance/new-relation/docs/session-refresh.md +24 -0
  101. package/skills/spec-wiki/evals/files/maintenance/new-relation/incoming/refresh-worker.ts +7 -0
  102. package/skills/spec-wiki/evals/files/maintenance/new-relation/src/session.ts +7 -0
  103. package/skills/spec-wiki/evals/files/maintenance/no-impact/docs/auth-contract.md +17 -0
  104. package/skills/spec-wiki/evals/files/maintenance/no-impact/incoming/telemetry-format.ts +3 -0
  105. package/skills/spec-wiki/evals/files/maintenance/no-impact/src/auth.ts +3 -0
  106. package/skills/spec-wiki/evals/files/query/docs/how-to-run-jobs.md +4 -0
  107. package/skills/spec-wiki/evals/files/query/docs/spec-overview.md +9 -0
  108. package/skills/spec-wiki/evals/files/query/features/jobs/cancellation-contract.md +15 -0
  109. package/skills/spec-wiki/evals/files/query/features/jobs/retry-spec.md +6 -0
  110. package/skills/spec-wiki/evals/maintenance_regression.py +311 -0
  111. package/skills/spec-wiki/evals/retrieval-eval.md +37 -0
  112. package/skills/spec-wiki/evals/retrieval-evals.json +37 -0
  113. package/skills/spec-wiki/evals/trigger-evals.json +94 -0
  114. package/skills/spec-wiki/references/bootstrap.md +100 -0
  115. package/skills/spec-wiki/references/focused-spec.md +117 -0
  116. package/skills/spec-wiki/references/index-schema.md +330 -0
  117. package/skills/spec-wiki/references/maintenance.md +134 -0
  118. package/skills/spec-wiki/references/query.md +74 -0
  119. package/skills/spec-wiki/scripts/spec_wiki.py +2076 -0
  120. package/docs/desktop-markdown-media.md +0 -77
  121. package/external/pi-tools-suite/docs/dcp-emergency-current-turn.md +0 -102
  122. package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +0 -54
  123. package/external/pi-tools-suite/src/skill-installer/index.ts +0 -333
  124. package/skills/spec-lite/SKILL.md +0 -140
package/README.md CHANGED
@@ -95,7 +95,7 @@ On startup, Pix tries to link the bundled suite into the active Pi agent directo
95
95
  | Context control | `dcp` | Explicit compression and context pruning for long sessions, including sidecar state. |
96
96
  | Local web access | `web-search` | Web search and page extraction through a local Ollama instance. |
97
97
  | Providers and quotas | `antigravity-auth`, `opencode-import`, `usage` | Antigravity OAuth/account failover, OpenCode credential import, and multi-provider usage views. |
98
- | Reusable workflows | `prompt-commands`, `skill-installer`, `session-name` | Prompt-command CRUD, skill installation/export, and session naming. |
98
+ | Reusable workflows | `prompt-commands`, `resource-registry`, `session-name` | Prompt-command CRUD, private Git-backed skill/agent install/update/push/remote-remove/local-uninstall with reload-on-change, project-scoped `.pi/tasks.jsonc` + `.pi/plans/` + `.pi/TODO.md` sync, a Desktop Registry sidebar for status/actions/configuration, and session naming. |
99
99
  | Model compatibility | `coding-discipline`, `model-tools`, `codex-reasoning-fix` | Model-specific discipline and vision lookup, compatibility aliases, and a Codex reasoning payload workaround. |
100
100
 
101
101
  Every module can be disabled. Optional integrations only activate when their requirements are available — for example, repository tools require an index, web tools require local Ollama web search, and LSP servers must be configured and trusted.
@@ -317,7 +317,7 @@ Type `/` for the live, searchable list. Extensions can add more commands than th
317
317
  | Status | `/usage` |
318
318
  | Process | `/quit`, `/exit` |
319
319
 
320
- Useful suite commands include `/todos`, `/todos-persist`, `/todos-scope`, `/sub-status`, `/sub-stop`, `/ultrawork`, `/hyperplan`, `/subagent-preset`, `/usage`, `/dcp`, `/idx-init`, `/idx-update`, `/opencode-import`, `/antigravity-add-account`, `/install-skill`, and `/export-skill`.
320
+ Useful suite commands include `/todos`, `/todos-persist`, `/todos-scope`, `/sub-status`, `/sub-stop`, `/ultrawork`, `/hyperplan`, `/subagent-preset`, `/usage`, `/dcp`, `/idx-init`, `/idx-update`, `/opencode-import`, `/antigravity-add-account`, and `/registry`.
321
321
 
322
322
  ### Keyboard and mouse
323
323
 
@@ -500,8 +500,16 @@ npm run test:tools-suite
500
500
 
501
501
  # Build the publishable renderer
502
502
  npm run build:pix
503
+
504
+ # Watch Pix, ACP, the bundled suite, and Desktop
505
+ npm run watch:all
503
506
  ```
504
507
 
508
+ `watch:all` rebuilds only the affected project parts. It keeps the last working
509
+ Desktop process alive while changes compile, then replaces it only after the
510
+ entire queued build succeeds. A failed build is reported without replacing the
511
+ running Desktop.
512
+
505
513
  Regenerate the README screenshots without accounts or live model traffic:
506
514
 
507
515
  ```bash
package/dist/app/app.d.ts CHANGED
@@ -51,6 +51,7 @@ export declare class PiUiExtendApp {
51
51
  * for one tab never reaches another tab's extensions.
52
52
  */
53
53
  private readonly extensionEventBusByRuntime;
54
+ private readonly subagentCatalogBySessionId;
54
55
  private readonly inputEditor;
55
56
  private lastInputEditorContentVersion;
56
57
  private readonly requestHistory;
@@ -78,6 +79,8 @@ export declare class PiUiExtendApp {
78
79
  private awaitCurrentSessionExtensions;
79
80
  private activateRuntime;
80
81
  private createExtensionEventBus;
82
+ private subagentTypesForRuntime;
83
+ private contextInventoryText;
81
84
  private handleTerminalBellAttention;
82
85
  private afterSessionReplacement;
83
86
  private requireRuntime;
package/dist/app/app.js CHANGED
@@ -47,6 +47,8 @@ import { checkPiUpdate, checkPixUpdate, formatPixStartupUpdateDialog, formatPiSt
47
47
  import { checkAndUpdateIdxOnStartup, formatIdxStartupUpdateNotice } from "./cli/startup-checks.js";
48
48
  import { AppVoiceController } from "./input/voice-controller.js";
49
49
  import { createIsolatedExtensionEventBus } from "./extensions/extension-event-bus.js";
50
+ import { parseSubagentCatalogState, SUBAGENTS_CATALOG_STATE_EVENT, } from "./extensions/subagent-catalog-state.js";
51
+ import { createReloadContextInventory, formatReloadContextInventory } from "./commands/reload-context-inventory.js";
50
52
  import { setAppIconTheme } from "./icons.js";
51
53
  const TERMINAL_BELL_ATTENTION_EVENT = "pix:terminal-bell:attention";
52
54
  function normalizeJumpTargetText(text) {
@@ -123,6 +125,7 @@ export class PiUiExtendApp {
123
125
  * for one tab never reaches another tab's extensions.
124
126
  */
125
127
  extensionEventBusByRuntime = new WeakMap();
128
+ subagentCatalogBySessionId = new Map();
126
129
  inputEditor = new InputEditor();
127
130
  lastInputEditorContentVersion = this.inputEditor.contentVersion;
128
131
  requestHistory;
@@ -221,6 +224,7 @@ export class PiUiExtendApp {
221
224
  restoreAutoUserMessages: (messages) => this.queuedMessages.restoreAutoUserMessages(messages),
222
225
  captureDeferredUserMessages: () => this.queuedMessages.captureDeferredUserMessages(),
223
226
  restoreDeferredUserMessages: (messages) => this.queuedMessages.restoreDeferredUserMessages(messages),
227
+ contextInventoryText: (runtime, heading) => this.contextInventoryText(runtime, heading),
224
228
  addEntry: (entry) => this.addEntry(entry),
225
229
  showToast: (message, kind) => this.showToast(message, kind),
226
230
  render: () => this.render(),
@@ -358,6 +362,7 @@ export class PiUiExtendApp {
358
362
  this.extensionActions = new AppExtensionActionsController({
359
363
  isRunning: () => this.running,
360
364
  runtime: () => this.runtime,
365
+ subagentTypes: (runtime) => this.subagentTypesForRuntime(runtime),
361
366
  getInput: () => this.input,
362
367
  setInput: (value) => this.setInput(value),
363
368
  awaitCurrentSessionExtensions: (runtime) => this.awaitCurrentSessionExtensions(runtime),
@@ -508,6 +513,7 @@ export class PiUiExtendApp {
508
513
  this.commandController = new AppCommandController({
509
514
  options: this.options,
510
515
  runtime: () => this.runtime,
516
+ subagentTypes: (runtime) => this.subagentTypesForRuntime(runtime),
511
517
  requestHistory: () => this.requestHistory,
512
518
  getInput: () => this.input,
513
519
  setInput: (value) => this.setInput(value),
@@ -800,6 +806,7 @@ export class PiUiExtendApp {
800
806
  handleExtensionError: (error) => this.extensionActions.handleExtensionError(error),
801
807
  handleSessionEvent: (event) => this.handleSessionEvent(event),
802
808
  bindAgentPause: (session) => this.agentPauseController.bind(session),
809
+ contextInventoryText: (runtime, heading) => this.contextInventoryText(runtime, heading),
803
810
  addEntry: (entry) => this.addEntry(entry),
804
811
  setStatus: (status) => this.setStatus(status),
805
812
  showToast: (message, kind) => this.showToast(message, kind),
@@ -945,10 +952,29 @@ export class PiUiExtendApp {
945
952
  this.handleTerminalBellAttention(data);
946
953
  if (channel === SUBAGENTS_LIVE_STATE_EVENT)
947
954
  this.subagentsWidgetController.observeLiveState(data);
955
+ if (channel === SUBAGENTS_CATALOG_STATE_EVENT) {
956
+ const catalog = parseSubagentCatalogState(data);
957
+ if (catalog?.sessionId)
958
+ this.subagentCatalogBySessionId.set(catalog.sessionId, catalog);
959
+ }
948
960
  if (channel === TODO_STATE_EVENT)
949
961
  this.todoWidgetController.observeLiveState(data);
950
962
  });
951
963
  }
964
+ subagentTypesForRuntime(runtime) {
965
+ const sessionId = runtime.session.sessionManager.getSessionId();
966
+ const catalog = this.subagentCatalogBySessionId.get(sessionId);
967
+ if (!catalog)
968
+ return undefined;
969
+ const model = runtime.session.model;
970
+ const currentModelRef = model ? `${model.provider}/${model.id}` : undefined;
971
+ if (catalog.model && currentModelRef && catalog.model !== currentModelRef)
972
+ return undefined;
973
+ return catalog.types;
974
+ }
975
+ contextInventoryText(runtime, heading) {
976
+ return formatReloadContextInventory(createReloadContextInventory(runtime, this.subagentTypesForRuntime(runtime)), heading);
977
+ }
952
978
  handleTerminalBellAttention(data) {
953
979
  const sessionFile = isRecord(data) && typeof data.sessionFile === "string" ? data.sessionFile : undefined;
954
980
  this.tabsController.markTerminalBellAttention(sessionFile);
@@ -7,6 +7,7 @@ export type DirectPopupMenu = Exclude<ActivePopupMenu, "slash">;
7
7
  export type CommandControllerHost = {
8
8
  readonly options: AppOptions;
9
9
  runtime(): AgentSessionRuntime | undefined;
10
+ subagentTypes?(runtime: AgentSessionRuntime): readonly string[] | undefined;
10
11
  awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
11
12
  requestHistory(): AppRequestHistory;
12
13
  getInput(): string;
@@ -4,6 +4,7 @@ import { getProjectPixConfigPath, savePixAutocompleteModel, savePixDefaultModel,
4
4
  import { createId } from "../id.js";
5
5
  import { isThinkingLevel, parseScopedModelRef } from "../model/model-ref.js";
6
6
  import { appendPixSystemDisplayEntry } from "../session/pix-system-message.js";
7
+ import { createReloadContextInventory, formatReloadContextInventory } from "./reload-context-inventory.js";
7
8
  export class ModelCommandActions {
8
9
  host;
9
10
  constructor(host) {
@@ -333,10 +334,14 @@ export class ModelCommandActions {
333
334
  await session.reload();
334
335
  if (!isCommandScopeActive(this.host, scope))
335
336
  return;
337
+ const runtime = scope.runtime;
338
+ const text = runtime
339
+ ? formatReloadContextInventory(createReloadContextInventory(runtime, this.host.subagentTypes?.(runtime)), `Reloaded resources after model change to ${ref}`)
340
+ : `Reloaded resources after model change to ${ref}`;
336
341
  this.host.addEntry({
337
342
  id: createId("system"),
338
343
  kind: "system",
339
- text: `Reloaded resources after model change to ${ref}`,
344
+ text,
340
345
  });
341
346
  this.host.toast.success("Model changed and resources reloaded");
342
347
  }
@@ -217,7 +217,7 @@ export function createSlashCommands(actions, host) {
217
217
  },
218
218
  {
219
219
  name: "reload",
220
- description: "Reload keybindings, extensions, skills, prompts, and themes",
220
+ description: "Reload resources and show model-available skills, tools, and agents",
221
221
  kind: "builtin",
222
222
  keywords: ["restart", "refresh", "resources", "extensions", "skills", "prompts"],
223
223
  run: () => actions.runReloadCommand(),
@@ -11,6 +11,7 @@ import { copyTextToClipboard } from "../screen/clipboard.js";
11
11
  import { formatAccountUsageReport, queryAccountUsageReport } from "../model/model-usage-status.js";
12
12
  import { checkGlobalPiInstall, checkPixUpdate, formatGlobalPiCheck, formatPixUpdateCheck, parsePixUpdateArgs, pixUpdateUsage, } from "../cli/update.js";
13
13
  import { createStartupInfoMessage } from "../cli/startup-info.js";
14
+ import { createReloadContextInventory, formatReloadContextInventory } from "./reload-context-inventory.js";
14
15
  import { getCompleteSessionStats } from "../session/session-stats.js";
15
16
  import { loadSessionTitleConfig } from "../../bundled-extensions/session-title/config.js";
16
17
  import { fallbackSessionTitleFromInput, firstUserMessageText, generateSessionTitleWithRuntime, sessionTitleModelRefs, } from "../../bundled-extensions/session-title/title-generation.js";
@@ -309,7 +310,8 @@ export class SessionCommandActions {
309
310
  if (!isCommandScopeActive(this.host, scope))
310
311
  return;
311
312
  this.host.setSessionStatus(runtime.session);
312
- this.host.addEntry({ id: createId("system"), kind: "system", text: "Reloaded keybindings, extensions, skills, prompts, themes" });
313
+ const inventory = createReloadContextInventory(runtime, this.host.subagentTypes?.(runtime));
314
+ this.host.addEntry({ id: createId("system"), kind: "system", text: formatReloadContextInventory(inventory) });
313
315
  this.host.toast.success("Reloaded resources");
314
316
  }
315
317
  catch (error) {
@@ -391,7 +393,7 @@ export class SessionCommandActions {
391
393
  this.host.addEntry({
392
394
  id: createId("system"),
393
395
  kind: "system",
394
- text: `Deleted session ${targetSessionId}. Started a new session. cwd=${runtime.cwd}`,
396
+ text: formatReloadContextInventory(createReloadContextInventory(runtime, this.host.subagentTypes?.(runtime)), `Deleted session ${targetSessionId}. Started a new session. cwd=${runtime.cwd}`),
395
397
  });
396
398
  if (runtime.modelFallbackMessage)
397
399
  this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
@@ -427,7 +429,11 @@ export class SessionCommandActions {
427
429
  return;
428
430
  }
429
431
  this.host.resetSessionView();
430
- this.host.addEntry({ id: createId("system"), kind: "system", text: `Started a new session. cwd=${runtime.cwd}` });
432
+ this.host.addEntry({
433
+ id: createId("system"),
434
+ kind: "system",
435
+ text: formatReloadContextInventory(createReloadContextInventory(runtime, this.host.subagentTypes?.(runtime)), `Started a new session. cwd=${runtime.cwd}`),
436
+ });
431
437
  if (runtime.modelFallbackMessage)
432
438
  this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
433
439
  for (const diag of runtime.diagnostics ?? []) {
@@ -0,0 +1,11 @@
1
+ import type { AgentSessionRuntime } from "@earendil-works/pi-coding-agent";
2
+ export interface ReloadContextInventory {
3
+ readonly model: string;
4
+ readonly skills: readonly string[];
5
+ readonly tools: readonly string[];
6
+ readonly agents: readonly string[] | undefined;
7
+ readonly skillsReadable: boolean;
8
+ readonly subagentsActive: boolean;
9
+ }
10
+ export declare function createReloadContextInventory(runtime: AgentSessionRuntime, agentTypes: readonly string[] | undefined): ReloadContextInventory;
11
+ export declare function formatReloadContextInventory(inventory: ReloadContextInventory, heading?: string): string;
@@ -0,0 +1,59 @@
1
+ export function createReloadContextInventory(runtime, agentTypes) {
2
+ const session = runtime.session;
3
+ const tools = unique(session.getActiveToolNames());
4
+ const skillsReadable = tools.includes("read") || tools.includes("bash");
5
+ const subagentsActive = tools.includes("subagents");
6
+ const skills = skillsReadable
7
+ ? unique(session.resourceLoader.getSkills().skills.map((skill) => skill.name)).sort(compareText)
8
+ : [];
9
+ const agents = subagentsActive && agentTypes !== undefined
10
+ ? unique(agentTypes).sort(compareText)
11
+ : subagentsActive
12
+ ? undefined
13
+ : [];
14
+ return {
15
+ model: currentModelLabel(runtime),
16
+ skills,
17
+ tools,
18
+ agents,
19
+ skillsReadable,
20
+ subagentsActive,
21
+ };
22
+ }
23
+ export function formatReloadContextInventory(inventory, heading = "Reloaded keybindings, extensions, skills, prompts, themes") {
24
+ return [
25
+ heading,
26
+ "",
27
+ `Model: ${inventory.model}`,
28
+ "",
29
+ `Skills (in context): ${inventory.skillsReadable ? listOrNone(inventory.skills) : "(none; read/bash inactive)"}`,
30
+ "",
31
+ `Tools (active): ${listOrNone(inventory.tools)}`,
32
+ "",
33
+ `Agents (available): ${formatAgents(inventory)}`,
34
+ ].join("\n");
35
+ }
36
+ function currentModelLabel(runtime) {
37
+ const model = runtime.session.model;
38
+ if (!model)
39
+ return "unavailable";
40
+ const thinking = runtime.session.thinkingLevel;
41
+ const modelRef = `${model.provider}/${model.id}`;
42
+ return !thinking || thinking === "off" ? modelRef : `${modelRef}:${thinking}`;
43
+ }
44
+ function formatAgents(inventory) {
45
+ if (!inventory.subagentsActive)
46
+ return "(none; subagents tool inactive)";
47
+ if (inventory.agents === undefined)
48
+ return "(catalog unavailable)";
49
+ return listOrNone(inventory.agents);
50
+ }
51
+ function listOrNone(values) {
52
+ return values.length > 0 ? values.join(", ") : "(none)";
53
+ }
54
+ function unique(values) {
55
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
56
+ }
57
+ function compareText(left, right) {
58
+ return left.localeCompare(right);
59
+ }
@@ -4,6 +4,7 @@ import type { Entry } from "../types.js";
4
4
  export type AppExtensionActionsHost = {
5
5
  isRunning(): boolean;
6
6
  runtime(): AgentSessionRuntime | undefined;
7
+ subagentTypes?(runtime: AgentSessionRuntime): readonly string[] | undefined;
7
8
  getInput(): string;
8
9
  setInput(value: string): void;
9
10
  awaitCurrentSessionExtensions(runtime?: AgentSessionRuntime): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { basename } from "node:path";
2
2
  import { createId } from "../id.js";
3
3
  import { logPixEvent } from "../logger.js";
4
+ import { createReloadContextInventory, formatReloadContextInventory } from "../commands/reload-context-inventory.js";
4
5
  export class AppExtensionActionsController {
5
6
  host;
6
7
  logExtensionError;
@@ -60,6 +61,11 @@ export class AppExtensionActionsController {
60
61
  if (!this.isSessionActive(runtime, session))
61
62
  return;
62
63
  this.host.setSessionStatus(session);
64
+ this.host.addEntry({
65
+ id: createId("system"),
66
+ kind: "system",
67
+ text: formatReloadContextInventory(createReloadContextInventory(runtime, this.host.subagentTypes?.(runtime)), "Reloaded resources"),
68
+ });
63
69
  this.host.showToast("Reloaded resources", "success");
64
70
  this.host.render();
65
71
  },
@@ -0,0 +1,9 @@
1
+ export declare const SUBAGENTS_CATALOG_STATE_EVENT = "pi-tools-suite:async-subagents:catalog";
2
+ export interface SubagentCatalogState {
3
+ readonly version: 1;
4
+ readonly sessionId?: string;
5
+ readonly sessionFile?: string;
6
+ readonly model?: string;
7
+ readonly types: readonly string[];
8
+ }
9
+ export declare function parseSubagentCatalogState(value: unknown): SubagentCatalogState | undefined;
@@ -0,0 +1,23 @@
1
+ export const SUBAGENTS_CATALOG_STATE_EVENT = "pi-tools-suite:async-subagents:catalog";
2
+ export function parseSubagentCatalogState(value) {
3
+ if (!isRecord(value) || value.version !== 1 || !Array.isArray(value.types))
4
+ return undefined;
5
+ if (!value.types.every((type) => typeof type === "string" && type.trim().length > 0))
6
+ return undefined;
7
+ if (value.sessionId !== undefined && typeof value.sessionId !== "string")
8
+ return undefined;
9
+ if (value.sessionFile !== undefined && typeof value.sessionFile !== "string")
10
+ return undefined;
11
+ if (value.model !== undefined && typeof value.model !== "string")
12
+ return undefined;
13
+ return {
14
+ version: 1,
15
+ ...(value.sessionId ? { sessionId: value.sessionId } : {}),
16
+ ...(value.sessionFile ? { sessionFile: value.sessionFile } : {}),
17
+ ...(value.model ? { model: value.model } : {}),
18
+ types: [...new Set(value.types.map((type) => type.trim()))],
19
+ };
20
+ }
21
+ function isRecord(value) {
22
+ return typeof value === "object" && value !== null && !Array.isArray(value);
23
+ }
@@ -2,7 +2,7 @@ import { stringDisplayWidth } from "../../terminal-width.js";
2
2
  import { SUBAGENTS_WIDGET_MAX_ROWS } from "../constants.js";
3
3
  import { ellipsizeDisplay, padOrTrimPlain, sanitizeText, wrapLine } from "./render-text.js";
4
4
  import { thinkingLevelThemeColor } from "./status-line-renderer.js";
5
- import { activeSubagentStates, formatElapsedSince, formatSubagentsPanelStats, subagentIcon, subagentModelThinkingLabel, subagentRunName, taskPreviewMap, } from "../subagents/subagents-model.js";
5
+ import { activeSubagentStates, formatElapsedSince, formatSubagentLastActivity, formatSubagentsPanelStats, subagentIcon, subagentModelThinkingLabel, subagentRunName, taskPreviewMap, } from "../subagents/subagents-model.js";
6
6
  import { formatTodoPanelStats, formatTodoTaskLine, hasOpenTodoTasks, shiftSegmentsToSlice, todoTaskLineSegments, visibleTodoTaskRows, visibleTodoTasks, } from "../todo/todo-model.js";
7
7
  export function renderTodoPanel(details, expanded, width, colors) {
8
8
  if (!details)
@@ -110,14 +110,16 @@ export function renderSubagentsPanel(state, expanded, width, colors) {
110
110
  const icon = subagentIcon(preview);
111
111
  const runLabel = visibleRun.showRunLabel ? `${subagentRunName(visibleRun.runDir)} ` : "";
112
112
  const prefix = `${runLabel}${icon} ${agent.id} ${model} `;
113
- const suffix = ` ${formatElapsedSince(agent.startedAt, now)}`;
113
+ const activity = formatSubagentLastActivity(agent.lastActivity);
114
+ const elapsed = formatElapsedSince(agent.startedAt, now);
115
+ const suffix = activity ? ` ${activity} ${elapsed}` : ` ${elapsed}`;
114
116
  const taskWidth = Math.max(8, rowWidth - stringDisplayWidth(prefix) - stringDisplayWidth(suffix));
115
117
  const taskText = ellipsizeDisplay(task, taskWidth);
116
118
  const text = `${prefix}${taskText}${suffix}`;
117
119
  lines.push({
118
120
  text: padOrTrimPlain(text, width),
119
121
  colorOverride: colors.muted,
120
- segments: subagentPanelLineSegments({ text, icon, agentId: agent.id, model, taskText, prefix, runLabel, status: agent.status }, colors),
122
+ segments: subagentPanelLineSegments({ text, icon, agentId: agent.id, model, taskText, prefix, runLabel, activity, elapsed, status: agent.status }, colors),
121
123
  target,
122
124
  });
123
125
  }
@@ -136,13 +138,17 @@ function subagentPanelLineSegments(input, colors) {
136
138
  const modelStart = input.text.indexOf(input.model, nameStart + input.agentId.length);
137
139
  const taskStart = input.prefix.length;
138
140
  const suffixStart = taskStart + input.taskText.length;
141
+ const elapsedStart = input.text.length - input.elapsed.length;
142
+ const activityStart = input.activity ? elapsedStart - input.activity.length - 1 : -1;
139
143
  return [
140
144
  ...(runLabelStart >= 0 ? [{ start: runLabelStart, end: runLabelStart + input.runLabel.length, foreground: colors.warning }] : []),
141
145
  { start: iconStart, end: iconStart + input.icon.length, foreground: subagentStatusColor(input.status, colors), bold: true },
142
146
  { start: nameStart, end: nameStart + input.agentId.length, foreground: colors.accent, bold: true },
143
147
  { start: modelStart, end: modelStart + input.model.length, foreground: colors.info },
144
148
  { start: taskStart, end: suffixStart, foreground: colors.muted },
145
- { start: suffixStart, end: input.text.length, foreground: colors.muted },
149
+ { start: suffixStart, end: input.activity ? activityStart : input.text.length, foreground: colors.muted },
150
+ ...(input.activity ? [{ start: activityStart, end: activityStart + input.activity.length, foreground: colors.info }] : []),
151
+ ...(input.activity ? [{ start: activityStart + input.activity.length, end: input.text.length, foreground: colors.muted }] : []),
146
152
  ].filter((segment) => segment.start >= 0 && segment.end > segment.start);
147
153
  }
148
154
  function subagentStatusColor(status, colors) {
@@ -27,6 +27,7 @@ export type AppSessionLifecycleHost = {
27
27
  handleExtensionError(error: ExtensionError): void;
28
28
  handleSessionEvent(event: AgentSessionEvent): void;
29
29
  bindAgentPause(session: AgentSession): void;
30
+ contextInventoryText?(runtime: AgentSessionRuntime, heading: string): string;
30
31
  addEntry(entry: Entry): void;
31
32
  setStatus(status: string): void;
32
33
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
@@ -44,6 +44,12 @@ export class AppSessionLifecycleController {
44
44
  await this.bindCurrentSession({ awaitExtensions: false });
45
45
  });
46
46
  await this.bindCurrentSession({ awaitExtensions: false });
47
+ if (runtime.session.messages.length === 0) {
48
+ await this.awaitCurrentSessionExtensions(runtime);
49
+ const text = this.host.contextInventoryText?.(runtime, `Empty tab ready. cwd=${runtime.cwd}`);
50
+ if (text)
51
+ this.host.addEntry({ id: createId("system"), kind: "system", text });
52
+ }
47
53
  if (runtime.modelFallbackMessage) {
48
54
  this.host.addEntry({ id: createId("system"), kind: "system", text: runtime.modelFallbackMessage });
49
55
  }
@@ -43,6 +43,7 @@ export type AppTabsControllerHost = {
43
43
  restoreAutoUserMessages?(messages: readonly SubmittedUserMessage[]): void;
44
44
  captureDeferredUserMessages?(): readonly SubmittedUserMessage[];
45
45
  restoreDeferredUserMessages?(messages: readonly SubmittedUserMessage[]): void;
46
+ contextInventoryText?(runtime: AgentSessionRuntime, heading: string): string;
46
47
  addEntry(entry: Entry): void;
47
48
  showToast(message: string, kind: "success" | "error" | "warning" | "info"): void;
48
49
  render(): void;
@@ -350,6 +350,12 @@ export class AppTabsController {
350
350
  this.scheduleProjectSessionRetention();
351
351
  this.scheduleTabPrewarm();
352
352
  await this.loadActiveSessionHistory(restoredRuntime, generation);
353
+ if (restoredRuntime.session.messages.length === 0) {
354
+ await this.host.awaitCurrentSessionExtensions?.(restoredRuntime);
355
+ const text = this.host.contextInventoryText?.(restoredRuntime, `Opened an empty tab. cwd=${restoredRuntime.cwd}`);
356
+ if (text)
357
+ this.host.addEntry({ id: createId("system"), kind: "system", text });
358
+ }
353
359
  this.scheduleRestoredTabTitleRefresh(restoredSessionPaths, generation);
354
360
  }
355
361
  openNewTab() {
@@ -436,6 +442,7 @@ export class AppTabsController {
436
442
  this.host.render();
437
443
  try {
438
444
  await this.host.activateRuntime(newRuntime, { awaitExtensions: false });
445
+ await this.host.awaitCurrentSessionExtensions?.(newRuntime);
439
446
  }
440
447
  catch (error) {
441
448
  if (this.pendingActiveTabId === targetTab.id)
@@ -477,7 +484,12 @@ export class AppTabsController {
477
484
  this.scheduleProjectSessionRetention();
478
485
  this.host.resetSessionView();
479
486
  this.restoreDeferredUserMessages(targetTab.id);
480
- this.host.addEntry({ id: createId("system"), kind: "system", text: `Opened a new tab. cwd=${newRuntime.cwd}` });
487
+ this.host.addEntry({
488
+ id: createId("system"),
489
+ kind: "system",
490
+ text: this.host.contextInventoryText?.(newRuntime, `Opened a new tab. cwd=${newRuntime.cwd}`)
491
+ ?? `Opened a new tab. cwd=${newRuntime.cwd}`,
492
+ });
481
493
  if (newRuntime.modelFallbackMessage)
482
494
  this.host.addEntry({ id: createId("system"), kind: "system", text: newRuntime.modelFallbackMessage });
483
495
  for (const diag of newRuntime.diagnostics ?? []) {
@@ -571,6 +583,7 @@ export class AppTabsController {
571
583
  this.host.render();
572
584
  try {
573
585
  await this.host.activateRuntime(newRuntime, { awaitExtensions: false });
586
+ await this.host.awaitCurrentSessionExtensions?.(newRuntime);
574
587
  }
575
588
  catch {
576
589
  this.pendingActiveTabId = undefined;
@@ -615,6 +628,11 @@ export class AppTabsController {
615
628
  void this.saveTabs();
616
629
  this.scheduleTabPrewarm();
617
630
  await this.loadActiveSessionHistory(newRuntime, generation);
631
+ if (newRuntime.session.messages.length === 0) {
632
+ const text = this.host.contextInventoryText?.(newRuntime, `Opened an empty tab. cwd=${newRuntime.cwd}`);
633
+ if (text)
634
+ this.host.addEntry({ id: createId("system"), kind: "system", text });
635
+ }
618
636
  return true;
619
637
  }
620
638
  forkSessionEntryInNewTab(entryId) {
@@ -1,7 +1,8 @@
1
- import { readFile, readdir, realpath, stat } from "node:fs/promises";
1
+ import { open, readFile, readdir, realpath, stat } from "node:fs/promises";
2
2
  import { join, resolve } from "node:path";
3
3
  import { SUBAGENT_PARENT_SESSION_FILE, SUBAGENTS_REGISTRY_FILE, SUBAGENTS_RUN_ROOT } from "../constants.js";
4
4
  import { isSubagentRegistry } from "./subagents-model.js";
5
+ const LAST_ACTIVITY_TAIL_BYTES = 64 * 1024;
5
6
  export function subagentsRegistryPath(cwd) {
6
7
  return join(cwd, SUBAGENTS_RUN_ROOT, SUBAGENTS_REGISTRY_FILE);
7
8
  }
@@ -110,6 +111,9 @@ async function readSubagentAgentState(runDir, agentId, options) {
110
111
  const finishedAt = await readTrimmedFile(join(agentDir, "finished_at"));
111
112
  if (finishedAt)
112
113
  state.finishedAt = finishedAt;
114
+ const lastActivity = await readLastActivity(join(agentDir, "progress.jsonl"));
115
+ if (lastActivity)
116
+ state.lastActivity = lastActivity;
113
117
  const retryPending = await fileExists(join(agentDir, "retry_pending"));
114
118
  const stopRequested = await fileExists(join(agentDir, "stop_requested"));
115
119
  if (retryPending && !stopRequested && state.status !== "running" && state.status !== "done") {
@@ -179,6 +183,61 @@ async function readNumberFile(filePath) {
179
183
  const value = Number.parseInt(raw, 10);
180
184
  return Number.isFinite(value) ? value : undefined;
181
185
  }
186
+ async function readLastActivity(filePath) {
187
+ let handle;
188
+ try {
189
+ handle = await open(filePath, "r");
190
+ const size = (await handle.stat()).size;
191
+ if (size <= 0)
192
+ return undefined;
193
+ const start = Math.max(0, size - LAST_ACTIVITY_TAIL_BYTES);
194
+ const buffer = Buffer.allocUnsafe(size - start);
195
+ const { bytesRead } = await handle.read(buffer, 0, buffer.length, start);
196
+ if (bytesRead <= 0)
197
+ return undefined;
198
+ const lines = buffer.toString("utf8", 0, bytesRead).split(/\r?\n/);
199
+ if (start > 0) {
200
+ const previousByte = Buffer.allocUnsafe(1);
201
+ const previousRead = await handle.read(previousByte, 0, 1, start - 1);
202
+ if (previousRead.bytesRead === 1 && previousByte[0] !== 0x0a)
203
+ lines.shift();
204
+ }
205
+ for (let index = lines.length - 1; index >= 0; index--) {
206
+ const line = lines[index]?.trim();
207
+ if (!line)
208
+ continue;
209
+ try {
210
+ const value = JSON.parse(line);
211
+ if (!isProgressActivityRecord(value))
212
+ continue;
213
+ const toolName = typeof value.toolName === "string" ? value.toolName.trim() : "";
214
+ if (toolName)
215
+ return { label: toolName, at: value.at };
216
+ if (value.type === "message_start" || value.type === "message_end")
217
+ return { label: "Thinking", at: value.at };
218
+ }
219
+ catch {
220
+ // Ignore malformed or partially written tail lines and keep scanning.
221
+ }
222
+ }
223
+ }
224
+ catch {
225
+ return undefined;
226
+ }
227
+ finally {
228
+ await handle?.close().catch(() => undefined);
229
+ }
230
+ return undefined;
231
+ }
232
+ function isProgressActivityRecord(value) {
233
+ if (typeof value !== "object" || value === null || Array.isArray(value))
234
+ return false;
235
+ const record = value;
236
+ return record.stage === "rpc_event"
237
+ && typeof record.at === "string"
238
+ && record.at.trim().length > 0
239
+ && typeof record.type === "string";
240
+ }
182
241
  async function countFileLines(filePath) {
183
242
  try {
184
243
  const content = await readFile(filePath, "utf8");
@@ -32,5 +32,6 @@ export declare function allSubagentStatesTerminal(agents: readonly SubagentAgent
32
32
  export declare function taskPreviewMap(tasks: readonly SubagentTaskPreview[] | undefined): Map<string, SubagentTaskPreview>;
33
33
  export declare function formatSubagentTimestamp(value: string | undefined): string | undefined;
34
34
  export declare function formatElapsedSince(value: string | undefined, now?: number): string;
35
+ export declare function formatSubagentLastActivity(activity: SubagentAgentState["lastActivity"]): string | undefined;
35
36
  export declare function subagentRunName(runDir: string): string;
36
37
  export declare function resolveSubagentRunDir(cwd: string, runDir: string): string;
@@ -43,6 +43,14 @@ export function isSubagentAgentState(value) {
43
43
  return false;
44
44
  if (value.retryCount !== undefined && typeof value.retryCount !== "number")
45
45
  return false;
46
+ if (value.lastActivity !== undefined) {
47
+ if (!isRecord(value.lastActivity))
48
+ return false;
49
+ if (typeof value.lastActivity.label !== "string" || !value.lastActivity.label.trim())
50
+ return false;
51
+ if (typeof value.lastActivity.at !== "string" || !value.lastActivity.at.trim())
52
+ return false;
53
+ }
46
54
  return true;
47
55
  }
48
56
  export function isSubagentTaskPreview(value) {
@@ -219,11 +227,19 @@ export function formatSubagentTimestamp(value) {
219
227
  return new Date(time).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" });
220
228
  }
221
229
  export function formatElapsedSince(value, now = Date.now()) {
230
+ return formatDurationSince(value, now) ?? "elapsed:—";
231
+ }
232
+ export function formatSubagentLastActivity(activity) {
233
+ if (!activity)
234
+ return undefined;
235
+ return activity.label.trim() || undefined;
236
+ }
237
+ function formatDurationSince(value, now) {
222
238
  if (!value)
223
- return "elapsed:—";
239
+ return undefined;
224
240
  const started = Date.parse(value);
225
241
  if (!Number.isFinite(started))
226
- return "elapsed:—";
242
+ return undefined;
227
243
  const seconds = Math.max(0, Math.floor((now - started) / 1000));
228
244
  if (seconds < 60)
229
245
  return `${seconds}s`;
@@ -139,6 +139,10 @@ export type SubagentStatus = (typeof SUBAGENT_STATUSES)[number];
139
139
  export type SubagentActiveStatus = (typeof SUBAGENT_ACTIVE_STATUSES)[number];
140
140
  export type SubagentTerminalStatus = (typeof SUBAGENT_TERMINAL_STATUSES)[number];
141
141
  export type SubagentRenderMode = (typeof SUBAGENT_RENDER_MODES)[number];
142
+ export type SubagentActivity = {
143
+ label: string;
144
+ at: string;
145
+ };
142
146
  export type SubagentAgentState = {
143
147
  id: string;
144
148
  status: SubagentStatus;
@@ -151,6 +155,7 @@ export type SubagentAgentState = {
151
155
  stderrLines?: number;
152
156
  eventLines?: number;
153
157
  retryCount?: number;
158
+ lastActivity?: SubagentActivity;
154
159
  };
155
160
  export type SubagentTaskPreview = {
156
161
  id: string;