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.
- package/README.md +116 -36
- package/extensions/coms/coms-p2p.ts +44 -1
- package/extensions/coms/coms-shared.ts +35 -0
- package/extensions/orchestrator/async-agents.ts +2 -3
- package/extensions/orchestrator/enforcement.ts +16 -8
- package/extensions/orchestrator/index.ts +34 -2
- package/extensions/orchestrator/project-settings.ts +6 -6
- package/extensions/orchestrator/review-learning.ts +4 -4
- package/extensions/orchestrator/rules.ts +11 -7
- package/extensions/orchestrator/status-bar.ts +2 -2
- package/extensions/orchestrator/subagent-tool.ts +9 -4
- package/extensions/orchestrator/trail.ts +745 -0
- package/extensions/orchestrator/utils.ts +44 -1
- package/extensions/pidash/browser-ops.ts +231 -0
- package/extensions/pidash/discord-answers.ts +8 -0
- package/extensions/pidash/discord-attachments.ts +8 -0
- package/extensions/pidash/discord-board.ts +75 -0
- package/extensions/pidash/discord-bot.ts +1811 -0
- package/extensions/pidash/discord-commands.ts +90 -0
- package/extensions/pidash/discord-config.ts +93 -0
- package/extensions/pidash/discord-format.ts +24 -0
- package/extensions/pidash/discord-jobs-view.ts +74 -0
- package/extensions/pidash/discord-reactions.ts +76 -0
- package/extensions/pidash/discord-render.ts +8 -0
- package/extensions/pidash/discord-sessions-view.ts +8 -0
- package/extensions/pidash/discord-state.ts +8 -0
- package/extensions/pidash/pidash-ui/dist/assets/index-B7e6Zyb5.js +23 -0
- package/extensions/pidash/pidash-ui/dist/assets/index-BKsBLUuf.css +2 -0
- package/extensions/pidash/pidash-ui/dist/index.html +2 -2
- package/extensions/pidash/pidash-ui/src/App.tsx +81 -15
- package/extensions/pidash/pidash-ui/src/components/ComsPanel.tsx +162 -0
- package/extensions/pidash/pidash-ui/src/components/InfoBar.tsx +162 -70
- package/extensions/pidash/pidash-ui/src/components/InputBar.tsx +5 -5
- package/extensions/pidash/pidash-ui/src/components/KeybindingSettings.tsx +3 -0
- package/extensions/pidash/pidash-ui/src/components/McpPanel.tsx +102 -0
- package/extensions/pidash/pidash-ui/src/components/MemoryPanel.tsx +68 -0
- package/extensions/pidash/pidash-ui/src/components/MessageList.tsx +103 -54
- package/extensions/pidash/pidash-ui/src/components/NewSessionDialog.tsx +102 -0
- package/extensions/pidash/pidash-ui/src/components/SessionSidebar.tsx +243 -83
- package/extensions/pidash/pidash-ui/src/components/SettingsPanel.tsx +310 -0
- package/extensions/pidash/pidash-ui/src/components/SidePanel.tsx +164 -0
- package/extensions/pidash/pidash-ui/src/components/TasksPanel.tsx +119 -0
- package/extensions/pidash/pidash-ui/src/hooks/useKeybindings.ts +24 -3
- package/extensions/pidash/pidash-ui/src/hooks/useKeyboardNavigation.ts +21 -4
- package/extensions/pidash/pidash-ui/src/hooks/useMessageHandler.ts +78 -8
- package/extensions/pidash/pidash-ui/src/lib/settings-edit.ts +73 -0
- package/extensions/pidash/pidash-ui/src/lib/theme.ts +53 -0
- package/extensions/pidash/pidash-ui/src/main.tsx +9 -0
- package/extensions/pidash/pidash-ui/src/types.ts +58 -0
- package/extensions/pidash/pidash.ts +202 -13
- package/extensions/pitasks/index.ts +60 -9
- package/extensions/pitasks/task-store.ts +37 -12
- package/extensions/shared/chat/chat-attachments.ts +192 -0
- package/extensions/shared/chat/chat-bridge.ts +420 -0
- package/extensions/shared/chat/chat-events.ts +124 -0
- package/extensions/shared/chat/chat-format.ts +136 -0
- package/extensions/shared/chat/chat-picker.ts +128 -0
- package/extensions/shared/chat/chat-prompts.ts +158 -0
- package/extensions/shared/chat/chat-sessions.ts +187 -0
- package/extensions/shared/chat/chat-state.ts +192 -0
- package/extensions/shared/chat/chat-types.ts +86 -0
- package/extensions/shared/create-runtime-provider.ts +1 -1
- package/extensions/shared/types.ts +7 -2
- package/extensions/shared/ui-bridge.ts +232 -0
- package/package.json +6 -6
- package/scripts/pidash-discord.ts +6 -588
- package/scripts/pidash-server.ts +9 -0
- package/scripts/pidash-session-state.ts +38 -3
- package/scripts/{graft-allow-scripts.mjs → trail-allow-scripts.mjs} +2 -2
- package/settings-keys.json +8 -8
- package/extensions/orchestrator/graft.ts +0 -356
- package/extensions/pidash/pidash-ui/dist/assets/index-CI6UL4g4.js +0 -23
- 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
|
-
| **
|
|
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,
|
|
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
|
-
###
|
|
428
|
+
### Trail-code code map (on by default)
|
|
429
429
|
|
|
430
|
-
|
|
431
|
-
`
|
|
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
|
-
"
|
|
459
|
+
"trail_enable": false
|
|
436
460
|
}
|
|
437
461
|
```
|
|
438
462
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
The
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
-
**
|
|
473
|
-
`@
|
|
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 `
|
|
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
|
|
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 `
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
| `
|
|
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
|
|
988
|
-
|
|
|
989
|
-
|
|
|
990
|
-
|
|
|
991
|
-
|
|
|
992
|
-
|
|
|
993
|
-
|
|
|
994
|
-
|
|
|
995
|
-
| `/
|
|
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: **
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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
|
|
583
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
50
|
-
|
|
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
|
-
"
|
|
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.
|
|
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", "
|
|
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: "
|
|
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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|