pi-orchestrator-config 4.20.1 → 4.22.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 (73) hide show
  1. package/README.md +116 -36
  2. package/extensions/coms/coms-p2p.ts +44 -1
  3. package/extensions/coms/coms-shared.ts +35 -0
  4. package/extensions/orchestrator/async-agents.ts +2 -3
  5. package/extensions/orchestrator/enforcement.ts +16 -8
  6. package/extensions/orchestrator/index.ts +34 -2
  7. package/extensions/orchestrator/project-settings.ts +6 -6
  8. package/extensions/orchestrator/review-learning.ts +4 -4
  9. package/extensions/orchestrator/rules.ts +11 -7
  10. package/extensions/orchestrator/status-bar.ts +2 -2
  11. package/extensions/orchestrator/subagent-tool.ts +9 -4
  12. package/extensions/orchestrator/trail.ts +745 -0
  13. package/extensions/orchestrator/utils.ts +44 -1
  14. package/extensions/pidash/browser-ops.ts +231 -0
  15. package/extensions/pidash/discord-answers.ts +8 -0
  16. package/extensions/pidash/discord-attachments.ts +8 -0
  17. package/extensions/pidash/discord-board.ts +75 -0
  18. package/extensions/pidash/discord-bot.ts +1811 -0
  19. package/extensions/pidash/discord-commands.ts +90 -0
  20. package/extensions/pidash/discord-config.ts +93 -0
  21. package/extensions/pidash/discord-format.ts +24 -0
  22. package/extensions/pidash/discord-jobs-view.ts +74 -0
  23. package/extensions/pidash/discord-reactions.ts +76 -0
  24. package/extensions/pidash/discord-render.ts +8 -0
  25. package/extensions/pidash/discord-sessions-view.ts +8 -0
  26. package/extensions/pidash/discord-state.ts +8 -0
  27. package/extensions/pidash/pidash-ui/dist/assets/index-B7e6Zyb5.js +23 -0
  28. package/extensions/pidash/pidash-ui/dist/assets/index-BKsBLUuf.css +2 -0
  29. package/extensions/pidash/pidash-ui/dist/index.html +2 -2
  30. package/extensions/pidash/pidash-ui/src/App.tsx +81 -15
  31. package/extensions/pidash/pidash-ui/src/components/ComsPanel.tsx +162 -0
  32. package/extensions/pidash/pidash-ui/src/components/InfoBar.tsx +162 -70
  33. package/extensions/pidash/pidash-ui/src/components/InputBar.tsx +5 -5
  34. package/extensions/pidash/pidash-ui/src/components/KeybindingSettings.tsx +3 -0
  35. package/extensions/pidash/pidash-ui/src/components/McpPanel.tsx +102 -0
  36. package/extensions/pidash/pidash-ui/src/components/MemoryPanel.tsx +68 -0
  37. package/extensions/pidash/pidash-ui/src/components/MessageList.tsx +103 -54
  38. package/extensions/pidash/pidash-ui/src/components/NewSessionDialog.tsx +102 -0
  39. package/extensions/pidash/pidash-ui/src/components/SessionSidebar.tsx +243 -83
  40. package/extensions/pidash/pidash-ui/src/components/SettingsPanel.tsx +310 -0
  41. package/extensions/pidash/pidash-ui/src/components/SidePanel.tsx +164 -0
  42. package/extensions/pidash/pidash-ui/src/components/TasksPanel.tsx +119 -0
  43. package/extensions/pidash/pidash-ui/src/hooks/useKeybindings.ts +24 -3
  44. package/extensions/pidash/pidash-ui/src/hooks/useKeyboardNavigation.ts +21 -4
  45. package/extensions/pidash/pidash-ui/src/hooks/useMessageHandler.ts +78 -8
  46. package/extensions/pidash/pidash-ui/src/lib/settings-edit.ts +73 -0
  47. package/extensions/pidash/pidash-ui/src/lib/theme.ts +53 -0
  48. package/extensions/pidash/pidash-ui/src/main.tsx +9 -0
  49. package/extensions/pidash/pidash-ui/src/types.ts +58 -0
  50. package/extensions/pidash/pidash.ts +202 -13
  51. package/extensions/pitasks/index.ts +60 -9
  52. package/extensions/pitasks/task-store.ts +37 -12
  53. package/extensions/shared/chat/chat-attachments.ts +192 -0
  54. package/extensions/shared/chat/chat-bridge.ts +420 -0
  55. package/extensions/shared/chat/chat-events.ts +124 -0
  56. package/extensions/shared/chat/chat-format.ts +136 -0
  57. package/extensions/shared/chat/chat-picker.ts +128 -0
  58. package/extensions/shared/chat/chat-prompts.ts +158 -0
  59. package/extensions/shared/chat/chat-sessions.ts +187 -0
  60. package/extensions/shared/chat/chat-state.ts +192 -0
  61. package/extensions/shared/chat/chat-types.ts +86 -0
  62. package/extensions/shared/create-runtime-provider.ts +1 -1
  63. package/extensions/shared/types.ts +7 -2
  64. package/extensions/shared/ui-bridge.ts +232 -0
  65. package/package.json +6 -6
  66. package/scripts/pidash-discord.ts +6 -588
  67. package/scripts/pidash-server.ts +9 -0
  68. package/scripts/pidash-session-state.ts +38 -3
  69. package/scripts/{graft-allow-scripts.mjs → trail-allow-scripts.mjs} +2 -2
  70. package/settings-keys.json +8 -8
  71. package/extensions/orchestrator/graft.ts +0 -356
  72. package/extensions/pidash/pidash-ui/dist/assets/index-CI6UL4g4.js +0 -23
  73. package/extensions/pidash/pidash-ui/dist/assets/index-hM6gC5Yg.css +0 -2
package/README.md CHANGED
@@ -26,7 +26,7 @@ Single extension that provides:
26
26
  | **Git status** | Live git status in status line with colored icons — updates after every tool call. Shows clickable `#N` when the branch has an open PR. Last-activity clock `⏱ HH:MM (Xm/Xh ago)` shows time since last response |
27
27
  | **Desktop notifications** | Notifies via `notify-send` on task completion, waiting for input, and action required |
28
28
  | **File preview** | Serves generated HTML/frontend files via HTTP for browser preview from container |
29
- | **Graft integration** | When `graft_enable=true`, main agents and subagents query the local Graft graph before raw project navigation on substantive prompts. Children only consume graphs. Stale graphs remain usable, retrieval failures fall back to raw tools, and main processes coordinate rebuilds with a cross-process lock. |
29
+ | **Trail-code integration** | On by default when the `trail` binary is installed. Main agents and subagents query the local code map before raw project navigation: the first project navigation per turn is gated behind a `trail_*` call, guidance is map-first (`trail_find_code` first, `trail_repo_map` once per session), and injected hits carry source. Worktrees resolve the map under `graft/` or `.trail/cache/`, inherit enablement from the main checkout, and seed from it instead of cold-building. Stale maps stay usable and failures fall open. |
30
30
  | **Pidash dashboard** | Live web dashboard — multi-session monitoring, browser messaging, model switching, live session name updates, reasoning token display |
31
31
  | **Pidiff viewer** | Per-project diff viewer with review comments — branch diffs, file tree, inline comments, git-based ignore rules |
32
32
  | **Dreaming** | Background memory consolidation — extracts memories from sessions, deduplicates, maintains topic-based memory |
@@ -179,7 +179,7 @@ The installer covers:
179
179
 
180
180
  - **Pi Packages** — pi-config, pi-vertex-claude, pi-web-access, myk-pi-tools, bun
181
181
  - **Python Tools** — prek
182
- - **npm Packages** — acpx, agent-browser, graft
182
+ - **npm Packages** — acpx, agent-browser, trail-code (`@trailhq/trail-code`)
183
183
  - **Browser Automation** — playwright + chromium
184
184
  - **Environment Setup** — gitignore configuration
185
185
 
@@ -425,28 +425,74 @@ Set `image_model` in `pi-config-settings.json` or use `PI_IMAGE_MODEL` env var.
425
425
 
426
426
  In containers, images are auto-served via HTTP for browser preview.
427
427
 
428
- ### Graft repository graph (optional)
428
+ ### Trail-code code map (on by default)
429
429
 
430
- Set `graft_enable` to `true` in `.pi/pi-config-settings.json` (or set
431
- `PI_GRAFT_ENABLE=true`) to enable local Graft repository graph context:
430
+ Trail-code is the local code map behind the `trail_*` tools. It is **on by
431
+ default** whenever the `trail` binary is on `PATH` — no setting required. In a
432
+ trusted git worktree, pi indexes the project and gives main agents and
433
+ subagents a queryable map, so the first project navigation of every substantive
434
+ turn is a `trail_*` query instead of a blind `grep`.
435
+
436
+ Install or upgrade the CLI:
437
+
438
+ ```bash
439
+ DO_NOT_TRACK=1 npm install -g @trailhq/trail-code@latest
440
+ uv run scripts/install.py # audited install used by the container
441
+ ```
442
+
443
+ The installer downloads `@trailhq/trail-code@latest` with scripts disabled into a
444
+ dedicated npm prefix, audits that isolated dependency tree for lifecycle scripts,
445
+ and strictly rebuilds it with only those packages approved. The completed prefix
446
+ is published under the global npm prefix and its `trail` binary linked globally,
447
+ so unrelated global packages are never evaluated. It verifies `trail --version`;
448
+ `DO_NOT_TRACK=1` disables Trail telemetry. Requirements: Node.js 22+, npm 12+,
449
+ and the node-gyp prerequisites (Python 3, `make`, G++). If the binary is missing
450
+ or the install fails, pi continues without the map and the `trail_*` tools are
451
+ simply absent.
452
+
453
+ **Kill switch.** `trail_enable` (env `PI_TRAIL_ENABLE`) defaults to `true`. Set it
454
+ to `false` in `.pi/pi-config-settings.json`, or globally in
455
+ `~/.pi/pi-config-settings.json`, to keep the integration fully inactive:
432
456
 
433
457
  ```json
434
458
  {
435
- "graft_enable": true
459
+ "trail_enable": false
436
460
  }
437
461
  ```
438
462
 
439
- Graft requires Node.js 22+, npm 12+, node-gyp prerequisites (Python 3, `make`, and
440
- G++), and the `graft` CLI. The container includes them; native installs can use
441
- `uv run scripts/install.py`. The installer downloads `@nanonets/graft@latest` with
442
- scripts disabled into a dedicated npm prefix, audits that isolated dependency tree
443
- for lifecycle scripts, and strictly rebuilds it with only those packages approved.
444
- The completed prefix is then published under the global npm prefix and its `graft`
445
- binary linked globally, so unrelated global packages are never evaluated. The
446
- installer verifies `graft --version`; `DO_NOT_TRACK=1` disables Graft telemetry.
447
- When enabled, Graft reads the trusted project and writes a regenerable `./graft/`
448
- cache. Delete that directory or set `graft_enable` to `false` to disable it. If the
449
- native CLI is unavailable or its installation fails, pi continues without Graft.
463
+ **Where the map lives.** Upstream owns the path: `./graft/` on older repos and
464
+ `.trail/cache/` once a repo is on trail (upstream renames a legacy map itself).
465
+ Both are regenerable and gitignored, and pi reads whichever is present. Never
466
+ commit them.
467
+
468
+ **Worktrees.** The map is resolved from the worktree root, and enablement is
469
+ inherited from the main checkout when the worktree has no `.pi` settings file
470
+ (`.pi/` is gitignored, so a fresh worktree would otherwise look disabled). A
471
+ fresh worktree seeds its own cache from the main checkout on the first query — a
472
+ copy plus branch-diff repair, never a cold rebuild — and never writes to the main
473
+ checkout's map. Expect roughly 48 MB per worktree in this repo.
474
+
475
+ **Layers.** This is not memory. Pi memory (`.pi/memory/`, dreaming, enforced
476
+ entries) is what pi carries between sessions. Trail's own layer —
477
+ `.trail/learnings/`, `trail note`, the pre-ship `trail check` — is upstream's and
478
+ **we do not use it**. We only read the code map.
479
+
480
+ ### Migrating from Graft
481
+
482
+ The integration was renamed from Graft to Trail-code. The break is intentional:
483
+
484
+ - Only `@trailhq/trail-code` is installed and only `trail` is invoked. The `graft`
485
+ binary is no longer installed and there is no fallback — if you only have
486
+ `@nanonets/graft`, install trail to get the map tools back.
487
+ - `graft_enable` / `PI_GRAFT_ENABLE` are gone, with no alias and no migration. An
488
+ existing `graft_enable: true` is a dead key, which is harmless: the feature is
489
+ default-on when the binary exists.
490
+ - Tools are `trail_find_code`, `trail_find_all`, `trail_file_api`,
491
+ `trail_trace_calls`, `trail_repo_map`, `trail_refresh`, `trail_blast_radius`.
492
+ - `log_graft` becomes `log_trail` (env `PI_LOG_TRAIL`); the old `~/.pi/logs/graft`
493
+ directory is neither read nor migrated.
494
+ - `graft/` survives as a _path_ until upstream renames it: it is the map
495
+ directory, not the product.
450
496
 
451
497
  ### Cache Miss Notices
452
498
 
@@ -469,20 +515,20 @@ Run pi inside a disposable container for **filesystem isolation** — the agent
469
515
  - **Consistent tooling** — All required tools pre-installed in a single image
470
516
  - **Disposable** — Container is destroyed after each session (`--rm`)
471
517
 
472
- **Graft (optional `graft_enable`):** The image installs the latest
473
- `@nanonets/graft` with `DO_NOT_TRACK=1`. It downloads with scripts disabled, derives
518
+ **Trail-code (`trail_enable`, on by default):** The image installs the latest
519
+ `@trailhq/trail-code` with `DO_NOT_TRACK=1`. It downloads with scripts disabled, derives
474
520
  an exact script allowlist by auditing the installed package's reachable dependency
475
521
  metadata in a dedicated npm prefix, then strictly rebuilds that isolated tree with
476
522
  those approvals. Only after the rebuild succeeds is the completed prefix moved under
477
- the global npm prefix and its `graft` binary linked globally. This prevents unrelated
523
+ the global npm prefix and its `trail` binary linked globally. This prevents unrelated
478
524
  global packages from entering npm's strict preflight. The allowlist is command-scoped
479
525
  and changes with `@latest`; other npm commands keep npm 12's install-script
480
526
  protections. The image includes Python 3,
481
- `make`, and the GCC/G++ toolchain required by node-gyp for Graft and other native
527
+ `make`, and the GCC/G++ toolchain required by node-gyp for Trail-code and other native
482
528
  npm packages; these remain available for reliable package rebuilds. The build then
483
- verifies `graft --version`. Enable Graft in trusted project or global pi-config
484
- settings; it reads the project locally and stores its regenerable graph in `./graft/`. Set
485
- `graft_enable` to `false` and delete that directory to disable and remove the cache.
529
+ verifies `trail --version`. The map is written to the mounted project's `./graft/` or
530
+ `.trail/cache/` directory; set `trail_enable` to `false` and delete that directory to
531
+ disable and remove the cache.
486
532
  Native installations require Node.js 22+, npm 12+, Python 3, `make`, and G++; the
487
533
  installer uses the same audited strict installation and failure remains non-fatal.
488
534
 
@@ -661,6 +707,14 @@ Pidash is a web-based dashboard that runs alongside the TUI, accessible from any
661
707
  - Per-project diff viewer (pidiff) — opens in browser via clickable `pi-diff` status-line link, powered by `@pierre/diffs` + `@pierre/trees`, with review comments
662
708
  - Collapsible thinking and tool blocks with copy buttons
663
709
  - ask_user tool bridging (answer from browser or TUI)
710
+ - **Full TUI parity (#968)** — every interactive prompt and surface is answerable/operable from either TUI or browser, first answer wins:
711
+ - Dangerous-command approvals, commit-trailer selection, project-agent confirmations, and `/tasks` menus render in both surfaces
712
+ - Tasks panel (view/create/start/complete/delete/clear) driven through the live TaskStore with live updates
713
+ - Coms peer list (busy state, queue depth, context %) + send message to a peer (same delivery path as `coms_send`)
714
+ - New-session creation (name + initial model) and `/compact` from the browser
715
+ - Memory status (usage, topics, situation report)
716
+ - MCP server management (list/login/logout)
717
+ - Full settings page — every key from `settings-keys.json`, project + global scopes, typed editors, written through the live settings module (same semantics as `/pi-config-settings`)
664
718
  - Mobile responsive
665
719
  - Event buffering for message replay on refresh
666
720
  - Browser push notifications for background events (configurable per event type)
@@ -781,7 +835,7 @@ PI_PIDIFF_ENABLE=false pi
781
835
  | `acpx` | Agent proxy for remote models |
782
836
  | `kubectl` / `oc` | Kubernetes and OpenShift CLI |
783
837
  | `agent-browser` | Browser automation CLI (navigate, click, screenshot, forms) |
784
- | `graft` | Local repository code graph for opt-in `graft_enable`; graph cache is stored in the mounted project’s `./graft/` directory |
838
+ | `trail` | Local code map for `trail_enable` (on by default); the regenerable cache lives in the mounted project's `./graft/` or `.trail/cache/` directory |
785
839
  | `procps` | Process utilities (ps, top, pgrep, pkill) |
786
840
  | `passwd` | `usermod`/`groupmod` — init remaps user `node` to `PI_HOST_UID`/`PI_HOST_GID` |
787
841
  | `docker` / `podman` | Container CLIs (used via `docker-safe` read-only wrapper) |
@@ -960,7 +1014,8 @@ Control your pi sessions from your phone via Discord DMs.
960
1014
  - Go to "Bot" section → click "Reset Token" → copy the token
961
1015
  - Enable "Message Content Intent" under "Privileged Gateway Intents"
962
1016
  - Go to "OAuth2 > URL Generator" → select scope `bot` → select permissions:
963
- `Send Messages`, `Read Message History`, `Add Reactions`, `View Channels`
1017
+ `Send Messages`, `Read Message History`, `Add Reactions`, `View Channels`,
1018
+ and for per-session threads: `Create Public Threads`, `Send Messages in Threads`
964
1019
  - Open the generated URL → add bot to your server
965
1020
 
966
1021
  2. **Configure:**
@@ -970,11 +1025,21 @@ Control your pi sessions from your phone via Discord DMs.
970
1025
  cat > ~/.pi/discord.env << EOF
971
1026
  DISCORD_BOT_TOKEN=your-token-here
972
1027
  DISCORD_ALLOWED_USERS=your-discord-user-id
1028
+ # Optional: open access without an allowlist (off by default)
1029
+ # DISCORD_ALLOW_ALL=true
1030
+ # Optional: how long a pending agent question waits (default 10m, min 30s)
1031
+ # DISCORD_PROMPT_TTL_MS=600000
1032
+ # Optional: forum channel id for /board (one post per session)
1033
+ # DISCORD_FORUM_CHANNEL_ID=
973
1034
  EOF
974
1035
  ```
975
1036
 
976
1037
  Find your Discord user ID: Settings > Advanced > Developer Mode ON, then right-click yourself > Copy User ID.
977
1038
 
1039
+ > The bot refuses to start without `DISCORD_ALLOWED_USERS` — a token alone must
1040
+ > not mean "anyone who DMs the bot drives your agent". Set
1041
+ > `DISCORD_ALLOW_ALL=true` only if you really want open access.
1042
+
978
1043
  3. **Restart pidash:**
979
1044
  The Discord bot runs inside the pidash server daemon. Restart it to pick up the config:
980
1045
 
@@ -984,15 +1049,30 @@ Control your pi sessions from your phone via Discord DMs.
984
1049
 
985
1050
  ### Discord Commands
986
1051
 
987
- | Command | Description |
988
- | ----------- | ----------------------------------- |
989
- | `!sessions` | List active pi sessions |
990
- | `!watch N` | Watch/switch to session N |
991
- | `!status` | Show current session info |
992
- | `!abort` | Abort current operation |
993
- | `!help` | Show commands |
994
- | Any text | Send as prompt to watched session |
995
- | `/command` | Forward slash command to pi session |
1052
+ | Command | Description |
1053
+ | -------------------- | -------------------------------------------------- |
1054
+ | `/sessions` | Paginated, ephemeral list with watch buttons |
1055
+ | `/watch session:<n>` | Watch a session by name (autocomplete) |
1056
+ | `/status` | Watched session: model, branch, state, cwd |
1057
+ | `/stop` | Interrupt the running turn |
1058
+ | `/compact` | Compact the watched session's context |
1059
+ | `/model [value]` | Show or switch the model |
1060
+ | `/thinking [value]` | Show or set the thinking level |
1061
+ | `/tasks` `/coms` `/memory` `/mcp` `/diff` | Inspect the watched session |
1062
+ | `/file path:<rel>` | Send a file from the session cwd (8MB max) |
1063
+ | `/notify on\|off` | DM when a turn finishes in an unwatched session |
1064
+ | Any text (DM/thread) | Send as a prompt to the watched session |
1065
+ | `/cancel` | Dismiss a pending agent question |
1066
+
1067
+ Agent questions arrive with **Approve/Deny** buttons or a dropdown; you can also
1068
+ reply `yes`/`no`, an option number, or the option text. In a server, watching a
1069
+ session opens a **thread** for it (DMs are used as a fallback).
1070
+
1071
+ The bot fails closed: without `DISCORD_ALLOWED_USERS` it refuses to start unless
1072
+ you set `DISCORD_ALLOW_ALL=true`. Commands, reaction shortcuts (⏹ 🔁 🗑), the
1073
+ forum session board and every environment variable are documented in
1074
+ [docs/discord-bot.md](docs/discord-bot.md)
1075
+ for the full surface.
996
1076
 
997
1077
  ### How It Works
998
1078
 
@@ -1013,7 +1093,7 @@ it also runs as a pre-commit hook when TypeScript files change.
1013
1093
 
1014
1094
  ## Prerequisites
1015
1095
 
1016
- - [pi](https://github.com/badlogic/pi-mono) (minimum version: **0.99.0**). Pidash uses Pi's `ui_prompt_start` and `ui_prompt_end` events to show prompt wait separately from agent work.
1096
+ - [pi](https://github.com/badlogic/pi-mono) (minimum version: **1.4.0**). Pidash uses Pi's `ui_prompt_start` and `ui_prompt_end` events to show prompt wait separately from agent work.
1017
1097
  - `gh` CLI (for GitHub operations)
1018
1098
  - `uv` (for Python execution)
1019
1099
  - `myk-pi-tools` (optional, for `/pr-review` and `/release`)
@@ -19,7 +19,8 @@ import type { AgentToolResult, ExtensionAPI, ExtensionContext, Theme } from "@ea
19
19
  import { Text, truncateToWidth } from "@earendil-works/pi-tui";
20
20
  import { Type } from "typebox";
21
21
  import { applyExtensionDefaults } from "./themeMap.js";
22
- import { ulid, hexFg, isValidHex, fallbackColor, comsParseYamlFrontmatter as parseFrontmatter, nowIso, abbreviateModel, findSystemPromptPath, readFrontmatterFromArgv, readTaskSummary, buildInboundContent, renderTasksPart, renderQueuePart, formatQueueStr, formatComsResponseText, formatComsResponseType, formatComsResponseBody, formatComsInboundType, sanitizeComsName, createComsInboundTasks, shouldEvictOnLeaving, shouldAnnouncePeerLeft, isSessionStillRegistered, FALLBACK_PALETTE, type TasksSummary, insertInboundByPriority, shouldAcceptSteer, summarizeBroadcast, formatBusyStr, selectSenderBundle, type BroadcastTargetResult } from "./coms-shared.js";
22
+ import {
23
+ buildComsPeersPayload, ulid, hexFg, isValidHex, fallbackColor, comsParseYamlFrontmatter as parseFrontmatter, nowIso, abbreviateModel, findSystemPromptPath, readFrontmatterFromArgv, readTaskSummary, buildInboundContent, renderTasksPart, renderQueuePart, formatQueueStr, formatComsResponseText, formatComsResponseType, formatComsResponseBody, formatComsInboundType, sanitizeComsName, createComsInboundTasks, shouldEvictOnLeaving, shouldAnnouncePeerLeft, isSessionStillRegistered, FALLBACK_PALETTE, type TasksSummary, insertInboundByPriority, shouldAcceptSteer, summarizeBroadcast, formatBusyStr, selectSenderBundle, type BroadcastTargetResult } from "./coms-shared.js";
23
24
  import { appendHistoryRecord, queryHistoryRecords, truncatePreview, type HistoryRecord } from "./coms-history.js";
24
25
  import { openListDetailOverlay, OverlayScrollDetail } from "../orchestrator/overlay-dashboard.js";
25
26
  import * as net from "node:net";
@@ -785,6 +786,7 @@ export default function (pi: ExtensionAPI) {
785
786
  const key = `pending=${pc}|busy=${busyKey}|prog=${progressKey}`;
786
787
  if (key === lastPoolSnapshot) return;
787
788
  lastPoolSnapshot = key;
789
+ emitComsPeers();
788
790
  try { installPoolWidget(currentCtx); } catch {}
789
791
  }
790
792
 
@@ -1803,6 +1805,10 @@ Do not respond to this message.`;
1803
1805
  existing.staleCount = 0;
1804
1806
  existing.model = entry.model ?? existing.model;
1805
1807
  log.debug("fs_watch_heartbeat", entry.name, "ctx", entry.context_used_pct, "busy", existing.busy);
1808
+ // Heartbeats mutate peer fields without touching the widget
1809
+ // refresh snapshot key — re-emit so browser peer cards stay
1810
+ // current (Qodo R1: stale peer status otherwise lingers).
1811
+ emitComsPeers();
1806
1812
  } else if (entry && entry.coms_session_id && !peerCards.has(entry.coms_session_id)) {
1807
1813
  const card: AgentCard & { staleCount: number } = {
1808
1814
  name: entry.name, purpose: entry.purpose, model: entry.model,
@@ -3613,6 +3619,43 @@ Do not respond to this message.`;
3613
3619
  handler: comsRefreshHandler,
3614
3620
  });
3615
3621
 
3622
+ // ━━ #968 part C: pidash browser bridge ─━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3623
+ // Peer list + messaging from the browser, routed through the SAME
3624
+ // resolveTarget/deliverPrompt path as coms_send — identical delivery
3625
+ // semantics, browser is just another transport.
3626
+ function emitComsPeers(): void {
3627
+ try {
3628
+ pi.events.emit("pidash:coms-peers", buildComsPeersPayload(
3629
+ peerCards.values(),
3630
+ identity ? { name: identity.name, purpose: identity.purpose, project: identity.project } : null,
3631
+ ));
3632
+ } catch { /* pidash not loaded */ }
3633
+ }
3634
+ pi.events.on("pidash:coms-peers-request", () => emitComsPeers());
3635
+ pi.events.on("pidash:coms-send", async (data: unknown) => {
3636
+ const d = data as { requestId?: string; target?: string; prompt?: string; steer?: boolean; priority?: boolean };
3637
+ if (!d?.target || !d?.prompt) {
3638
+ pi.events.emit("pidash:coms-send-result", { requestId: d?.requestId, ok: false, error: "target and prompt required" });
3639
+ return;
3640
+ }
3641
+ try {
3642
+ if (!identity) throw new Error("coms not initialised");
3643
+ const target = await resolveTarget(d.target);
3644
+ if (!target) throw new Error(`peer not found: ${d.target}`);
3645
+ const { msg_id } = await deliverPrompt(target, {
3646
+ prompt: d.prompt,
3647
+ ...(d.steer ? { steer: true } : {}),
3648
+ ...(d.priority ? { priority: true } : {}),
3649
+ });
3650
+ log.info("pidash_coms_send", { target: target.name, msg_id, steer: !!d.steer });
3651
+ pi.events.emit("pidash:coms-send-result", { requestId: d.requestId, ok: true, msg_id, target: target.name });
3652
+ } catch (e) {
3653
+ const error = e instanceof Error ? e.message : String(e);
3654
+ log.warn("pidash_coms_send_failed", { target: d.target, error });
3655
+ pi.events.emit("pidash:coms-send-result", { requestId: d.requestId, ok: false, error });
3656
+ }
3657
+ });
3658
+
3616
3659
  // ━━ /coms-queue command — view and kill queued messages ━━━━━━━━━━━━━━━
3617
3660
  let comsQueueCmdHandler: any;
3618
3661
  pi.registerCommand("coms-queue", {
@@ -551,6 +551,41 @@ export function formatComsResponseText(targetName: string, response: any, error:
551
551
  : `[coms response from ${targetName}${arrow}${queueNote}] ${JSON.stringify(response, null, 2)}`;
552
552
  }
553
553
 
554
+ /** #968 C: pidash browser peer-list payload — pure projection of live cards. */
555
+ export interface ComsPeerCard {
556
+ name: string;
557
+ purpose: string;
558
+ model: string;
559
+ color: string;
560
+ context_used_pct?: number;
561
+ queue_depth?: number;
562
+ busy?: boolean;
563
+ tasks_summary?: unknown;
564
+ staleCount: number;
565
+ }
566
+
567
+ export function buildComsPeersPayload(
568
+ cards: Iterable<ComsPeerCard>,
569
+ self: { name: string; purpose?: string; project?: string } | null,
570
+ ): { self: { name: string; purpose: string; project: string } | null; peers: Array<Record<string, unknown>> } {
571
+ const peers: Array<Record<string, unknown>> = [];
572
+ for (const card of cards) {
573
+ peers.push({
574
+ name: card.name,
575
+ purpose: card.purpose,
576
+ model: card.model,
577
+ busy: card.busy ?? false,
578
+ queueDepth: card.queue_depth ?? 0,
579
+ contextUsedPct: card.context_used_pct ?? 0,
580
+ tasksSummary: card.tasks_summary ?? null,
581
+ });
582
+ }
583
+ return {
584
+ self: self ? { name: self.name, purpose: self.purpose || "", project: self.project || "" } : null,
585
+ peers,
586
+ };
587
+ }
588
+
554
589
  /** Sanitize a coms identity name for safe embedding in header strings.
555
590
  * Strips all control characters (C0/C1), brackets; replaces whitespace with hyphens. */
556
591
  export function sanitizeComsName(name: string): string {
@@ -17,7 +17,7 @@ const require = createRequire(import.meta.url);
17
17
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
18
  import type { AgentConfig, AgentDiscoveryResult } from "./agents.js";
19
19
  import { resolveAgentModelProvider } from "./resolve-agent-model.js";
20
- import { withGraftTools } from "./graft.js";
20
+ import { trailToolsArgs } from "./trail.js";
21
21
  import { getPiInvocation, getProjectTmpDir, parseProcStartTime, djb2Hash } from "./utils.js";
22
22
  import { addReviewerPending, recordReviewerResult, countFindings, readReviewState } from "./pi-config-review-state.js";
23
23
  import {
@@ -1097,8 +1097,7 @@ export function registerAsyncAgents(
1097
1097
  piArgs.push("--session-id", deterministicSessionId);
1098
1098
  if (effectiveModel) piArgs.push("--model", effectiveModel);
1099
1099
  if (effectiveProvider) piArgs.push("--provider", effectiveProvider);
1100
- const tools = withGraftTools(agent.tools, getSetting(cwd, "graft_enable"));
1101
- if (tools?.length) piArgs.push("--tools", tools.join(","));
1100
+ piArgs.push(...trailToolsArgs(agent.tools, cwd));
1102
1101
 
1103
1102
  if (agent.systemPrompt?.trim()) {
1104
1103
  const promptPath = path.join(workerDir, "system-prompt.md");
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import { createLogger } from "../shared/logger.js";
9
+ import { createBridgedUi } from "../shared/ui-bridge.js";
9
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
10
11
  import { isToolCallEventType } from "@earendil-works/pi-coding-agent";
11
12
  import { Type } from "typebox";
@@ -119,6 +120,9 @@ function commitTrailerModelId(ctx: any): string {
119
120
  }
120
121
 
121
122
  /** Inject commit trailer into command (Assisted-by, Co-authored-by, etc.) */
123
+ /** #968 B: set by registerEnforcement; null → plain ctx.ui prompts (tests, unregistered). */
124
+ let bridgedUi: ReturnType<typeof createBridgedUi> | null = null;
125
+
122
126
  async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd: string): Promise<EnforcementResult> {
123
127
  // #932: rewrite a literal `$PI_MODEL` in the trailer identity — an agent
124
128
  // following the git-expert trailer instruction may write the placeholder
@@ -159,10 +163,9 @@ async function handleCommitTrailer(command: string, event: any, ctx: any, gitCwd
159
163
  // No UI available — default to first option
160
164
  trailerName = options[0];
161
165
  } else {
162
- const selected = await ctx.ui.select(
163
- "Select commit trailer:",
164
- options,
165
- );
166
+ const selected = bridgedUi
167
+ ? await bridgedUi.select(ctx, "Select commit trailer:", options)
168
+ : await ctx.ui.select("Select commit trailer:", options);
166
169
  if (!selected || !options.includes(selected)) {
167
170
  return {
168
171
  block: true,
@@ -579,10 +582,10 @@ async function checkDangerousCommands(command: string, cwd: string, ctx: any): P
579
582
  : []),
580
583
  "No",
581
584
  ];
582
- const ok = await ctx.ui.select(
583
- `⚠️ Dangerous statement${remaining.length > 1 ? "s" : ""} (of the full command):\n\n${list}${scopeNote}\n\nAllow?`,
584
- options,
585
- );
585
+ const promptTitle = `⚠️ Dangerous statement${remaining.length > 1 ? "s" : ""} (of the full command):\n\n${list}${scopeNote}\n\nAllow?`;
586
+ const ok = bridgedUi
587
+ ? await bridgedUi.select(ctx, promptTitle, options)
588
+ : await ctx.ui.select(promptTitle, options);
586
589
  if (ok === "No" || ok === undefined)
587
590
  return { block: true, reason: "Blocked by user. Do NOT retry with an equivalent command (e.g., find -delete, perl, python os.remove). Stop and report this block to the user." };
588
591
  if (ok === "Yes — run once" || ok.startsWith("Always allow all of the above")) {
@@ -639,6 +642,11 @@ async function checkDangerousCommands(command: string, cwd: string, ctx: any): P
639
642
  }
640
643
 
641
644
  export function registerEnforcement(pi: ExtensionAPI, inContainer?: boolean): void {
645
+ // #968 part B: enforcement prompts (dangerous-command approvals, trailer
646
+ // selection) are bridged — answerable from TUI or pidash browser, first
647
+ // answer wins, same enforcement semantics either way. Module-level check
648
+ // functions read this ref; null (unregistered / tests) falls back to plain ui.
649
+ bridgedUi = createBridgedUi(pi);
642
650
 
643
651
  pi.on("session_start", (_event, ctx) => {
644
652
  cachedTrailerSelection = null;
@@ -34,8 +34,10 @@ import { registerMemoryTools } from "./memory-tools.js";
34
34
  import { registerReviewUI } from "./review-ui.js";
35
35
  import { registerSessionSearch } from "./session-search.js";
36
36
  import { registerSettingsTui } from "./settings-tui.js";
37
+ import { buildSituationReport } from "./situation-report.js";
38
+ import { listTopics } from "./memory-tree.js";
37
39
  import { registerMarkdownTransformer } from "./markdown-transformer.js";
38
- import { registerGraft } from "./graft.js";
40
+ import { registerTrail } from "./trail.js";
39
41
  import { registerReviewLearning } from "./review-learning.js";
40
42
  import { ensureGitSshTimeout, isRunningInContainer, terminalNotify } from "./utils.js";
41
43
  import { createLogger } from "../shared/logger.js";
@@ -89,6 +91,36 @@ export default function (pi: ExtensionAPI) {
89
91
  }
90
92
  });
91
93
 
94
+ // ── #968 part E: memory status surface for pidash ──────────────────
95
+ // Read-only snapshot through the live orchestrator memory modules —
96
+ // situation report, topic hotness, entry counts, budget usage.
97
+ // The requesting session's cwd comes in the event payload (the pidash
98
+ // browser may be watching a session in a different project than this
99
+ // process's cwd).
100
+ pi.events.on("pidash:memory-status-request", (data: unknown) => {
101
+ try {
102
+ const payload = (typeof data === "object" && data !== null ? data : {}) as { cwd?: unknown };
103
+ const cwd = typeof payload.cwd === "string" && payload.cwd ? payload.cwd : process.cwd();
104
+ const report = buildSituationReport(cwd);
105
+ const topics = listTopics(cwd).map((t) => ({
106
+ name: t.name,
107
+ entryCount: t.entryCount,
108
+ hotness: Math.round(t.hotness),
109
+ newestEntry: t.newestEntry,
110
+ }));
111
+ const usageMatch = report.match(/# Project Memory \[([0-9]+)%[^\]]*\]/);
112
+ pi.events.emit("pidash:memory-status", {
113
+ topics,
114
+ usagePct: usageMatch ? Number(usageMatch[1]) : null,
115
+ report,
116
+ });
117
+ log.info("pidash_memory_status_sent", { topics: topics.length });
118
+ } catch (e: any) {
119
+ log.warn("pidash_memory_status_failed", { error: e?.message });
120
+ pi.events.emit("pidash:memory-status", { topics: [], usagePct: null, report: "", error: e?.message });
121
+ }
122
+ });
123
+
92
124
  // Extended autocomplete must register FIRST — it wraps registerCommand
93
125
  // to inject getArgumentCompletions before other modules register their commands.
94
126
  registerExtendedAutocomplete(pi);
@@ -97,7 +129,7 @@ export default function (pi: ExtensionAPI) {
97
129
  const { spawnAsyncAgent, killAsyncAgent, getAsyncJobs } = registerAsyncAgents(pi, terminalNotify);
98
130
  registerSubagentTool(pi, spawnAsyncAgent, killAsyncAgent);
99
131
  registerProjectSettings(pi);
100
- registerGraft(pi);
132
+ registerTrail(pi);
101
133
  registerEnforcement(pi, IN_CONTAINER);
102
134
  registerReviewUI(pi);
103
135
  registerRules(pi, getAsyncJobs);
@@ -46,8 +46,8 @@ interface ProjectSettings {
46
46
  cli_agents?: string | string[];
47
47
  pidash_enable?: boolean;
48
48
  pidiff_enable?: boolean;
49
- /** Enable local Graft repository graph integration. Default: false. */
50
- graft_enable?: boolean;
49
+ /** Enable local Trail code map integration. Default: true. */
50
+ trail_enable?: boolean;
51
51
  pidash_port?: number;
52
52
  task_auto_clear_enabled?: boolean;
53
53
  task_auto_clear_minutes?: number;
@@ -123,7 +123,7 @@ const PROJECT_SETTINGS_KEYS: (keyof ProjectSettings)[] = [
123
123
  "cli_agents",
124
124
  "pidash_enable",
125
125
  "pidiff_enable",
126
- "graft_enable",
126
+ "trail_enable",
127
127
  "pidash_port",
128
128
  "task_auto_clear_enabled",
129
129
  "task_auto_clear_minutes",
@@ -185,7 +185,7 @@ export function parseSettingsFile(filePath: string): ProjectSettings {
185
185
  if (typeof raw.orchestrator_edit_write_block === "boolean") result.orchestrator_edit_write_block = raw.orchestrator_edit_write_block;
186
186
  if (typeof raw.pidash_enable === "boolean") result.pidash_enable = raw.pidash_enable;
187
187
  if (typeof raw.pidiff_enable === "boolean") result.pidiff_enable = raw.pidiff_enable;
188
- if (typeof raw.graft_enable === "boolean") result.graft_enable = raw.graft_enable;
188
+ if (typeof raw.trail_enable === "boolean") result.trail_enable = raw.trail_enable;
189
189
  if (typeof raw.pidash_port === "number" && Number.isInteger(raw.pidash_port) && raw.pidash_port > 0 && raw.pidash_port <= 65535) {
190
190
  result.pidash_port = raw.pidash_port;
191
191
  }
@@ -238,7 +238,7 @@ export function parseSettingsFile(filePath: string): ProjectSettings {
238
238
  const SPECIAL_CASE_KEYS = new Set<string>([
239
239
  "commit_trailer", "allow_push_to_protected_branches", "use_worktrees",
240
240
  "dream_interval_hours", "dco", "comment_signature", "review_loop_enforcement",
241
- "orchestrator_edit_write_block", "pidash_enable", "pidiff_enable", "graft_enable", "pidash_port",
241
+ "orchestrator_edit_write_block", "pidash_enable", "pidiff_enable", "trail_enable", "pidash_port",
242
242
  "image_model", "internal_operations_provider", "internal_operations_model",
243
243
  "review_loop_max_cycles", "acpx_agents", "cli_agents", "agent_provider",
244
244
  "agent_model", "agent_overrides",
@@ -485,7 +485,7 @@ export function getSetting(cwd: string, key: "acpx_agents"): string[];
485
485
  export function getSetting(cwd: string, key: "cli_agents"): string[];
486
486
  export function getSetting(cwd: string, key: "pidash_enable"): boolean;
487
487
  export function getSetting(cwd: string, key: "pidiff_enable"): boolean;
488
- export function getSetting(cwd: string, key: "graft_enable"): boolean;
488
+ export function getSetting(cwd: string, key: "trail_enable"): boolean;
489
489
  export function getSetting(cwd: string, key: "pidash_port"): number;
490
490
  export function getSetting(cwd: string, key: "task_auto_clear_enabled"): boolean;
491
491
  export function getSetting(cwd: string, key: "task_auto_clear_minutes"): number;
@@ -1391,12 +1391,12 @@ export function createReviewLearning(options: ReviewLearningOptions = {}) {
1391
1391
  }
1392
1392
  });
1393
1393
 
1394
- // Surface 1 — before_agent_start injection (graft-gate pattern).
1394
+ // Surface 1 — before_agent_start injection (trail-gate pattern).
1395
1395
  // Purely mechanical path extraction (prompt tokens + branch diff), sync
1396
1396
  // DB/git with timeouts, no model calls. Diff paths are repo-relative —
1397
1397
  // normalized against the worktree ROOT so nested sessions still match
1398
1398
  // the DB's repo-relative paths. Must be fast and never throw — other
1399
- // before_agent_start handlers (graft gate, rules) must not stall.
1399
+ // before_agent_start handlers (trail gate, rules) must not stall.
1400
1400
  pi.on("before_agent_start", (event: any, ctx: any) => {
1401
1401
  try {
1402
1402
  const cwd = ctx?.cwd;
@@ -1415,7 +1415,7 @@ export function createReviewLearning(options: ReviewLearningOptions = {}) {
1415
1415
  recall.fullBlockInjected = true;
1416
1416
  log.info("recall_injected_prompt", { paths: findings.length, chars: block.length });
1417
1417
  // Prefer a custom prompt section (rules.ts pattern — preserves cached
1418
- // prompt prefixes); fall back to the graft return shape when a prior
1418
+ // prompt prefixes); fall back to a replacement prompt when a prior
1419
1419
  // handler forced the whole prompt or sections are unavailable.
1420
1420
  const opts = event?.systemPromptOptions;
1421
1421
  const sections = opts?.sections;
@@ -1430,7 +1430,7 @@ export function createReviewLearning(options: ReviewLearningOptions = {}) {
1430
1430
  }
1431
1431
  });
1432
1432
 
1433
- // Surface 2 — first-write tool_result hint (graft blastHint pattern):
1433
+ // Surface 2 — first-write tool_result hint (trail blastHint pattern):
1434
1434
  // catches paths discovered mid-run that prompt extraction missed. Appends
1435
1435
  // once per path per session; a path is checked once per session whether
1436
1436
  // or not it has findings (a no-findings path must not re-query the DB on