@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.21
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/CHANGELOG.md +292 -0
- package/README.md +8 -11
- package/docs/prefix-cache-architecture.md +14 -0
- package/package.json +22 -24
- package/packages/ask-user/README.md +55 -107
- package/packages/ask-user/ask-ui.ts +306 -732
- package/packages/ask-user/clipboard.ts +50 -0
- package/packages/ask-user/config.ts +74 -78
- package/packages/ask-user/package.json +2 -2
- package/packages/ask-user/questions.ts +151 -0
- package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
- package/packages/ask-user/tools.ts +198 -432
- package/packages/autocomplete/package.json +1 -1
- package/packages/autocomplete/src/constants.ts +38 -43
- package/packages/autocomplete/src/sorting.ts +8 -0
- package/packages/background-tasks/package.json +2 -2
- package/packages/background-tasks/src/config.ts +0 -73
- package/packages/background-tasks/src/index.ts +20 -16
- package/packages/background-tasks/src/registry.ts +1 -171
- package/packages/background-tasks/src/types.ts +0 -32
- package/packages/btw/README.md +13 -56
- package/packages/btw/extensions/btw.ts +403 -1796
- package/packages/btw/package.json +5 -2
- package/packages/btw/skills/btw/SKILL.md +7 -143
- package/packages/compactor/README.md +56 -287
- package/packages/compactor/package.json +5 -9
- package/packages/compactor/skills/compactor/SKILL.md +13 -20
- package/packages/compactor/src/card.ts +90 -0
- package/packages/compactor/src/commands/index.ts +73 -130
- package/packages/compactor/src/compaction/compact-args.ts +5 -3
- package/packages/compactor/src/compaction/filter-noise.ts +1 -1
- package/packages/compactor/src/compaction/hooks.ts +487 -311
- package/packages/compactor/src/compaction/source.ts +178 -0
- package/packages/compactor/src/compaction/summarize.ts +586 -52
- package/packages/compactor/src/config/manager.ts +130 -127
- package/packages/compactor/src/config/schema.ts +32 -48
- package/packages/compactor/src/index.ts +40 -431
- package/packages/compactor/src/info-screen.ts +44 -190
- package/packages/compactor/src/session/recall-blocks.ts +1 -1
- package/packages/compactor/src/stats.ts +76 -0
- package/packages/compactor/src/tools/context-budget.ts +17 -34
- package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
- package/packages/compactor/src/tools/register.ts +29 -269
- package/packages/compactor/src/types.ts +47 -186
- package/packages/core/README.md +25 -0
- package/packages/core/child-agent.ts +1 -0
- package/packages/core/command-echo.ts +63 -0
- package/packages/core/compaction-context.ts +41 -0
- package/packages/core/constants.ts +8 -38
- package/packages/core/events.ts +0 -53
- package/packages/core/fusion-status.ts +4 -0
- package/packages/core/global.d.ts +10 -0
- package/packages/core/harness-messages.ts +392 -0
- package/packages/core/index.ts +14 -0
- package/packages/core/kanboard-status.ts +49 -0
- package/packages/core/long-horizon-owner-status.ts +46 -0
- package/packages/core/model-cache.ts +81 -7
- package/packages/core/package.json +6 -3
- package/packages/core/src/attach/detect.ts +163 -0
- package/packages/core/src/child-agent/child-args.ts +31 -0
- package/packages/core/src/child-agent/delivery.ts +41 -0
- package/packages/core/src/child-agent/index.ts +5 -0
- package/packages/core/src/child-agent/readers.ts +203 -0
- package/packages/core/src/child-agent/runtime.ts +584 -0
- package/packages/core/src/evidence.ts +78 -0
- package/packages/core/src/hints/browser.ts +278 -0
- package/packages/core/src/hints/crab-data.ts +19 -0
- package/packages/core/src/hints/index.ts +865 -0
- package/packages/core/src/hints/lines.ts +607 -0
- package/packages/core/src/hints/store.ts +120 -0
- package/packages/core/src/jev/chatter.ts +24 -0
- package/packages/core/src/jev/settings.ts +177 -14
- package/packages/core/src/package-colors.ts +2 -0
- package/packages/core/src/settings/catalog.ts +39 -15
- package/packages/core/src/settings/engine.ts +48 -2
- package/packages/core/src/settings/hub.ts +83 -20
- package/packages/core/src/settings/open.ts +49 -0
- package/packages/core/src/settings/schema.ts +27 -7
- package/packages/core/src/tui/hub-kit.ts +18 -5
- package/packages/core/src/tui/kit.ts +198 -0
- package/packages/core/src/tui/progress.ts +73 -0
- package/packages/core/src/turn/arbiter.ts +290 -0
- package/packages/footer/README.md +4 -6
- package/packages/footer/package.json +3 -3
- package/packages/footer/src/glance-editor.ts +19 -11
- package/packages/footer/src/help.ts +1 -1
- package/packages/footer/src/index.ts +36 -3
- package/packages/footer/src/presets.ts +3 -4
- package/packages/footer/src/rendering/icons.ts +0 -20
- package/packages/footer/src/segments/compactor.ts +52 -142
- package/packages/footer/src/segments/status-ext.ts +0 -1
- package/packages/fusion/README.md +14 -4
- package/packages/fusion/package.json +4 -3
- package/packages/fusion/src/child-args.ts +1 -0
- package/packages/fusion/src/index.ts +161 -8
- package/packages/fusion/src/pi-settings.ts +57 -0
- package/packages/fusion/src/picker.ts +186 -51
- package/packages/fusion/src/preset.ts +56 -2
- package/packages/fusion/src/prompts.ts +1 -1
- package/packages/fusion/src/sidekick-runtime.ts +1 -420
- package/packages/fusion/src/tools.ts +107 -128
- package/packages/fusion/src/transcript.ts +27 -85
- package/packages/info-screen/package.json +2 -2
- package/packages/input-shortcuts/README.md +18 -30
- package/packages/input-shortcuts/package.json +2 -2
- package/packages/input-shortcuts/src/burst.ts +88 -0
- package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
- package/packages/input-shortcuts/src/cleared-input.ts +97 -0
- package/packages/input-shortcuts/src/index.ts +99 -193
- package/packages/input-shortcuts/src/last-response.ts +40 -0
- package/packages/input-shortcuts/src/registers.ts +8 -23
- package/packages/input-shortcuts/src/types.ts +4 -16
- package/packages/input-shortcuts/src/undo-redo.ts +59 -47
- package/packages/kanboard/README.md +51 -47
- package/packages/kanboard/index.ts +293 -55
- package/packages/kanboard/package.json +8 -8
- package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
- package/packages/kanboard/src/badges.ts +93 -0
- package/packages/kanboard/src/bin.ts +4 -2
- package/packages/kanboard/src/commands.ts +218 -84
- package/packages/kanboard/src/debug.ts +20 -0
- package/packages/kanboard/src/guard.ts +267 -109
- package/packages/kanboard/src/monitor.ts +330 -0
- package/packages/kanboard/src/notice-buffer.ts +74 -0
- package/packages/kanboard/src/progress.ts +27 -0
- package/packages/kanboard/src/reminders.ts +234 -0
- package/packages/kanboard/src/settings.ts +69 -15
- package/packages/kanboard/src/shapes.ts +5 -0
- package/packages/long-horizon/index.ts +190 -13
- package/packages/long-horizon/package.json +3 -3
- package/packages/long-horizon/src/commands.ts +103 -7
- package/packages/long-horizon/src/compaction-brief.ts +35 -0
- package/packages/long-horizon/src/engine/continuation.ts +84 -15
- package/packages/long-horizon/src/engine/goal-state.ts +64 -13
- package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
- package/packages/long-horizon/src/engine/ralph.ts +3 -2
- package/packages/long-horizon/src/engine/runaway.ts +68 -11
- package/packages/long-horizon/src/engine/verifier.ts +19 -0
- package/packages/long-horizon/src/gate.ts +37 -12
- package/packages/long-horizon/src/judge/resolve.ts +1 -0
- package/packages/long-horizon/src/owner.ts +35 -2
- package/packages/long-horizon/src/progress.ts +87 -0
- package/packages/long-horizon/src/prompts/goal.ts +1 -1
- package/packages/long-horizon/src/runtime.ts +172 -18
- package/packages/long-horizon/src/settings.ts +54 -39
- package/packages/long-horizon/src/tools/goal.ts +6 -0
- package/packages/long-horizon/src/tools/graph.ts +1 -1
- package/packages/long-horizon/src/tools/swarm.ts +3 -3
- package/packages/mcp/package.json +2 -2
- package/packages/memory/commands.ts +7 -2
- package/packages/memory/index.ts +210 -32
- package/packages/memory/package.json +3 -3
- package/packages/memory/save-session.ts +264 -0
- package/packages/memory/settings.ts +17 -0
- package/packages/memory/tools.ts +253 -203
- package/packages/notify/package.json +2 -2
- package/packages/skill-registry/README.md +53 -16
- package/packages/skill-registry/index.ts +230 -0
- package/packages/skill-registry/package.json +19 -8
- package/packages/skill-registry/src/editor.ts +206 -0
- package/packages/skill-registry/src/judge.ts +289 -0
- package/packages/skill-registry/src/registry.ts +87 -0
- package/packages/skill-registry/src/settings.ts +259 -0
- package/packages/skill-registry/src/vault.ts +80 -0
- package/packages/subagents/README.md +39 -119
- package/packages/subagents/package.json +6 -13
- package/packages/subagents/skills/subagents/SKILL.md +18 -42
- package/packages/subagents/src/agents.ts +213 -0
- package/packages/subagents/src/cards.ts +182 -0
- package/packages/subagents/src/index.ts +609 -1084
- package/packages/subagents/src/manager.ts +429 -0
- package/packages/subagents/src/profiles.ts +130 -0
- package/packages/subagents/src/transcript.ts +154 -0
- package/packages/subagents/src/ui.ts +389 -0
- package/packages/unipi/bundled.js +39756 -45569
- package/packages/unipi/index.ts +55 -10
- package/packages/updater/package.json +2 -2
- package/packages/utility/README.md +76 -69
- package/packages/utility/package.json +2 -2
- package/packages/utility/skills/image/SKILL.md +37 -0
- package/packages/utility/skills/utility/SKILL.md +17 -60
- package/packages/utility/src/answer/extract.ts +128 -0
- package/packages/utility/src/answer/index.ts +213 -0
- package/packages/utility/src/answer/reply.ts +133 -0
- package/packages/utility/src/answer/web.ts +167 -0
- package/packages/utility/src/attach/index.ts +205 -0
- package/packages/utility/src/commands.ts +38 -142
- package/packages/utility/src/diagnostics/engine.ts +50 -5
- package/packages/utility/src/image/index.ts +33 -0
- package/packages/utility/src/image/models.ts +221 -0
- package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
- package/packages/utility/src/image/settings.ts +178 -0
- package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
- package/packages/utility/src/image/tools.ts +168 -0
- package/packages/utility/src/image/transports.ts +186 -0
- package/packages/utility/src/index.ts +68 -389
- package/packages/utility/src/lifecycle/cleanup.ts +116 -311
- package/packages/utility/src/rename/gate.ts +102 -0
- package/packages/utility/src/rename/index.ts +191 -0
- package/packages/utility/src/rename/session.ts +170 -0
- package/packages/utility/src/render/delegated.ts +285 -0
- package/packages/utility/src/render/harness.ts +683 -0
- package/packages/utility/src/render/parse.ts +156 -0
- package/packages/utility/src/render/pi-settings.ts +38 -0
- package/packages/utility/src/render/reply-bg.ts +307 -0
- package/packages/utility/src/render/simple.ts +724 -0
- package/packages/utility/src/render/spacing.ts +235 -0
- package/packages/utility/src/render/styled.ts +251 -0
- package/packages/utility/src/render/tools.ts +353 -0
- package/packages/utility/src/settings.ts +166 -233
- package/packages/utility/src/types.ts +1 -222
- package/packages/watchdog/index.ts +17 -6
- package/packages/watchdog/package.json +3 -3
- package/packages/watchdog/src/config.ts +3 -2
- package/packages/web-api/package.json +2 -2
- package/packages/workflow/README.md +3 -0
- package/packages/workflow/index.ts +0 -2
- package/packages/workflow/package.json +2 -2
- package/packages/workflow/src/permission/decide.ts +6 -4
- package/packages/workflow/src/permission/index.ts +17 -7
- package/packages/workflow/src/permission/settings.ts +39 -4
- package/packages/workflow/src/plan/index.ts +19 -4
- package/packages/background-tasks/extensions/delegate-child.ts +0 -1
- package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
- package/packages/background-tasks/src/context-token-budget.ts +0 -903
- package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
- package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
- package/packages/background-tasks/src/delegate/budget.ts +0 -415
- package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
- package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
- package/packages/background-tasks/src/delegate/launch.ts +0 -484
- package/packages/background-tasks/src/delegate/result-package.ts +0 -459
- package/packages/background-tasks/src/delegate/runner.ts +0 -430
- package/packages/background-tasks/src/delegate/seed.ts +0 -423
- package/packages/background-tasks/src/delegate/types.ts +0 -323
- package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
- package/packages/background-tasks/src/delegate-extension.ts +0 -654
- package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
- package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
- package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
- package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
- package/packages/compactor/src/compaction/build-sections.ts +0 -76
- package/packages/compactor/src/compaction/extract/goals.ts +0 -36
- package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
- package/packages/compactor/src/compaction/format.ts +0 -53
- package/packages/compactor/src/compaction/merge.ts +0 -143
- package/packages/compactor/src/config/presets.ts +0 -160
- package/packages/compactor/src/executor/executor.ts +0 -233
- package/packages/compactor/src/executor/runtime.ts +0 -115
- package/packages/compactor/src/security/evaluator.ts +0 -89
- package/packages/compactor/src/security/policy.ts +0 -96
- package/packages/compactor/src/security/scanner.ts +0 -65
- package/packages/compactor/src/session/auto-inject.ts +0 -60
- package/packages/compactor/src/session/db.ts +0 -325
- package/packages/compactor/src/session/extract.ts +0 -144
- package/packages/compactor/src/session/resume-inject.ts +0 -64
- package/packages/compactor/src/session/snapshot.ts +0 -327
- package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
- package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
- package/packages/compactor/src/tools/ctx-execute.ts +0 -23
- package/packages/compactor/src/tools/ctx-stats.ts +0 -96
- package/packages/image/README.md +0 -99
- package/packages/image/package.json +0 -61
- package/packages/image/skills/image/SKILL.md +0 -76
- package/packages/image/src/generate.ts +0 -256
- package/packages/image/src/index.ts +0 -135
- package/packages/image/src/models.ts +0 -519
- package/packages/image/src/openai-images-api.ts +0 -282
- package/packages/image/src/register-providers.ts +0 -209
- package/packages/image/src/settings.ts +0 -173
- package/packages/image/src/tools.ts +0 -326
- package/packages/input-shortcuts/src/clipboard.ts +0 -119
- package/packages/kanboard/src/runner.ts +0 -817
- package/packages/skill-registry/skills/auto/SKILL.md +0 -465
- package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
- package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
- package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
- package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
- package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
- package/packages/skill-registry/skills/debug/SKILL.md +0 -228
- package/packages/skill-registry/skills/document/SKILL.md +0 -120
- package/packages/skill-registry/skills/fix/SKILL.md +0 -212
- package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
- package/packages/skill-registry/skills/plan/SKILL.md +0 -231
- package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
- package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
- package/packages/skill-registry/skills/research/SKILL.md +0 -251
- package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
- package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
- package/packages/skill-registry/skills/work/SKILL.md +0 -218
- package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
- package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
- package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
- package/packages/subagents/agents/delegate.md +0 -14
- package/packages/subagents/agents/oracle.md +0 -78
- package/packages/subagents/agents/researcher.md +0 -52
- package/packages/subagents/agents/reviewer.md +0 -80
- package/packages/subagents/agents/scout.md +0 -50
- package/packages/subagents/agents/worker.md +0 -59
- package/packages/subagents/prompts/council.md +0 -48
- package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
- package/packages/subagents/prompts/parallel-cleanup.md +0 -59
- package/packages/subagents/prompts/parallel-research.md +0 -50
- package/packages/subagents/prompts/parallel-review.md +0 -54
- package/packages/subagents/prompts/review-loop.md +0 -43
- package/packages/subagents/skills/council-mode/SKILL.md +0 -230
- package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
- package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
- package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
- package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
- package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
- package/packages/subagents/src/acceptance.ts +0 -331
- package/packages/subagents/src/agent-manager.ts +0 -405
- package/packages/subagents/src/agent-memory.ts +0 -300
- package/packages/subagents/src/agent-overrides.ts +0 -183
- package/packages/subagents/src/agent-runner.ts +0 -271
- package/packages/subagents/src/async-runner.ts +0 -422
- package/packages/subagents/src/authority-policy.ts +0 -47
- package/packages/subagents/src/budgets.ts +0 -285
- package/packages/subagents/src/child-safety.ts +0 -140
- package/packages/subagents/src/config.ts +0 -249
- package/packages/subagents/src/conversation-viewer.ts +0 -281
- package/packages/subagents/src/core-compat.ts +0 -94
- package/packages/subagents/src/custom-agents.ts +0 -343
- package/packages/subagents/src/file-system-retry.ts +0 -50
- package/packages/subagents/src/fleet-data.ts +0 -105
- package/packages/subagents/src/fleet-view.ts +0 -322
- package/packages/subagents/src/foreground-detach.ts +0 -59
- package/packages/subagents/src/fork-context.ts +0 -285
- package/packages/subagents/src/global.d.ts +0 -7
- package/packages/subagents/src/guide.ts +0 -129
- package/packages/subagents/src/mission-state.ts +0 -133
- package/packages/subagents/src/mission-store.ts +0 -436
- package/packages/subagents/src/output-limits.ts +0 -142
- package/packages/subagents/src/parity-types.ts +0 -315
- package/packages/subagents/src/pi-args.ts +0 -167
- package/packages/subagents/src/result-files.ts +0 -232
- package/packages/subagents/src/result-watcher.ts +0 -221
- package/packages/subagents/src/retained-children.ts +0 -176
- package/packages/subagents/src/run-fanout-budget.ts +0 -356
- package/packages/subagents/src/scheduled-runs.ts +0 -349
- package/packages/subagents/src/schemas.ts +0 -263
- package/packages/subagents/src/slash-commands.ts +0 -80
- package/packages/subagents/src/supervisor-channel.ts +0 -249
- package/packages/subagents/src/tool-handler.ts +0 -1144
- package/packages/subagents/src/types.ts +0 -238
- package/packages/subagents/src/widget.ts +0 -457
- package/packages/subagents/src/workflow-script.ts +0 -737
- package/packages/subagents/src/workflow-worker.ts +0 -384
- package/packages/subagents/src/worktree.ts +0 -614
- package/packages/utility/src/analytics/collector.ts +0 -293
- package/packages/utility/src/info-screen.ts +0 -80
- package/packages/utility/src/lifecycle/process.ts +0 -162
- package/packages/utility/src/prefix-cache.ts +0 -263
- package/packages/utility/src/skill-discovery.ts +0 -542
- package/packages/utility/src/tools/env.ts +0 -113
- package/packages/utility/src/tui/name-badge-state.ts +0 -315
- package/packages/utility/src/tui/name-badge.ts +0 -109
- /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — response formatting helpers (pure)
|
|
3
|
+
*
|
|
4
|
+
* Splits a shell command into shell text and embedded code (heredoc bodies,
|
|
5
|
+
* `python -c "…"`, `node -e '…'`) with the embedded language, parses pi's
|
|
6
|
+
* display diff, and pulls exit codes and test summaries out of tool output.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface Segment {
|
|
10
|
+
kind: "shell" | "code";
|
|
11
|
+
text: string;
|
|
12
|
+
lang: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const RUNTIME_LANG: Array<[RegExp, string]> = [
|
|
16
|
+
[/\b(?:python3?|uv run python|ipython)\b/, "python"],
|
|
17
|
+
[/\b(?:node|deno|bun|tsx|ts-node)\b/, "javascript"],
|
|
18
|
+
[/\b(?:psql|sqlite3|mysql|duckdb|clickhouse-client)\b/, "sql"],
|
|
19
|
+
[/\b(?:ruby|irb)\b/, "ruby"],
|
|
20
|
+
[/\b(?:php)\b/, "php"],
|
|
21
|
+
[/\b(?:perl)\b/, "perl"],
|
|
22
|
+
[/\b(?:lua|luajit)\b/, "lua"],
|
|
23
|
+
[/\b(?:bash|sh|zsh|ssh\s+\S+)\b/, "bash"],
|
|
24
|
+
[/\bjq\b/, "json"],
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const EXT_LANG: Record<string, string> = {
|
|
28
|
+
py: "python", js: "javascript", mjs: "javascript", cjs: "javascript", ts: "typescript", mts: "typescript", tsx: "tsx", jsx: "jsx",
|
|
29
|
+
json: "json", yaml: "yaml", yml: "yaml", toml: "toml", sql: "sql", sh: "bash", bash: "bash", rs: "rust", go: "go",
|
|
30
|
+
rb: "ruby", md: "markdown", html: "html", css: "css", xml: "xml", ini: "ini", conf: "ini", dockerfile: "dockerfile",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Language of code fed to the command before the heredoc marker. */
|
|
34
|
+
export function langForPrefix(prefix: string): string {
|
|
35
|
+
const line = prefix.split("\n").pop() ?? prefix;
|
|
36
|
+
const target = line.match(/(?:>|tee\s+(?:-a\s+)?)\s*["']?([^\s"'|;&]+)["']?\s*(?:<<|$)/)?.[1];
|
|
37
|
+
const ext = target?.split(".").pop()?.toLowerCase();
|
|
38
|
+
if (ext && EXT_LANG[ext]) return EXT_LANG[ext]!;
|
|
39
|
+
for (const [re, lang] of RUNTIME_LANG) if (re.test(line)) return lang;
|
|
40
|
+
return "text";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Shell text and embedded code, in order. Heredocs (`<<EOF`, `<<'EOF'`,
|
|
45
|
+
* `<<-"EOF"`) take the language of their command; `-c/-e` string arguments
|
|
46
|
+
* of known runtimes are highlighted as that runtime's language.
|
|
47
|
+
*/
|
|
48
|
+
export function splitCommand(command: string): Segment[] {
|
|
49
|
+
const out: Segment[] = [];
|
|
50
|
+
const push = (kind: Segment["kind"], text: string, lang: string) => {
|
|
51
|
+
if (!text) return;
|
|
52
|
+
const last = out.at(-1);
|
|
53
|
+
if (last && last.kind === kind && last.lang === lang) last.text += text;
|
|
54
|
+
else out.push({ kind, text, lang });
|
|
55
|
+
};
|
|
56
|
+
const heredoc = /<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\1[^\n]*\n/g;
|
|
57
|
+
let at = 0;
|
|
58
|
+
for (let m = heredoc.exec(command); m; m = heredoc.exec(command)) {
|
|
59
|
+
const tag = m[2]!;
|
|
60
|
+
const bodyStart = m.index + m[0].length;
|
|
61
|
+
const end = new RegExp(`^[\\t ]*${tag}[\\t ]*$`, "m");
|
|
62
|
+
const rest = command.slice(bodyStart);
|
|
63
|
+
const close = end.exec(rest);
|
|
64
|
+
if (!close) continue;
|
|
65
|
+
pushInline(command.slice(at, bodyStart), push);
|
|
66
|
+
push("code", rest.slice(0, close.index), langForPrefix(command.slice(0, m.index)));
|
|
67
|
+
at = bodyStart + close.index;
|
|
68
|
+
heredoc.lastIndex = at;
|
|
69
|
+
}
|
|
70
|
+
pushInline(command.slice(at), push);
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `python -c "…"` / `node -e '…'` → code segment; the rest stays shell. */
|
|
75
|
+
function pushInline(text: string, push: (kind: Segment["kind"], text: string, lang: string) => void): void {
|
|
76
|
+
const inline = /\b(python3?|node|deno|bun|ruby|perl|psql|sqlite3)\b([^\n'"]*?\s-(?:c|e|-eval|-command)\s+)(["'])([\s\S]*?)\3/g;
|
|
77
|
+
let at = 0;
|
|
78
|
+
for (let m = inline.exec(text); m; m = inline.exec(text)) {
|
|
79
|
+
const quoteStart = m.index + m[1]!.length + m[2]!.length;
|
|
80
|
+
push("shell", text.slice(at, quoteStart + 1), "bash");
|
|
81
|
+
push("code", m[4]!, langForPrefix(m[1]!));
|
|
82
|
+
push("shell", m[3]!, "bash");
|
|
83
|
+
at = m.index + m[0].length;
|
|
84
|
+
}
|
|
85
|
+
push("shell", text.slice(at), "bash");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface DiffRow {
|
|
89
|
+
kind: "add" | "del" | "ctx" | "gap";
|
|
90
|
+
line?: number;
|
|
91
|
+
text: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** pi's display diff (`+12 text`, `-12 text`, ` 12 text`, `...`) → rows. */
|
|
95
|
+
export function parseDiff(diff: string): DiffRow[] {
|
|
96
|
+
const rows: DiffRow[] = [];
|
|
97
|
+
for (const raw of diff.split("\n")) {
|
|
98
|
+
const m = raw.match(/^([+\- ])(\s*\d+) (.*)$/);
|
|
99
|
+
if (m) {
|
|
100
|
+
rows.push({ kind: m[1] === "+" ? "add" : m[1] === "-" ? "del" : "ctx", line: Number(m[2]), text: m[3]! });
|
|
101
|
+
} else if (/^\s*\.\.\.\s*$/.test(raw) || /^[+\- ]\s*\.\.\.$/.test(raw)) {
|
|
102
|
+
rows.push({ kind: "gap", text: "" });
|
|
103
|
+
} else if (raw.length) {
|
|
104
|
+
rows.push({ kind: "ctx", text: raw });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return rows;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function diffStats(rows: readonly DiffRow[]): { added: number; removed: number } {
|
|
111
|
+
return { added: rows.filter((r) => r.kind === "add").length, removed: rows.filter((r) => r.kind === "del").length };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Exit code from pi's bash error text; 0 for a successful result. */
|
|
115
|
+
export function exitCode(text: string, isError: boolean): number | undefined {
|
|
116
|
+
const m = text.match(/Command exited with code (\d+)\s*$/);
|
|
117
|
+
if (m) return Number(m[1]);
|
|
118
|
+
if (/Command (?:aborted|terminated without an exit code)\s*$/.test(text)) return undefined;
|
|
119
|
+
return isError ? 1 : 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Output without pi's trailing status line. */
|
|
123
|
+
export function stripStatus(text: string): string {
|
|
124
|
+
return text.replace(/\n*(?:Command exited with code \d+|Command aborted|Command terminated without an exit code)\s*$/, "");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** "83 passed · 0 failed" from common test runners' summary lines. */
|
|
128
|
+
export function testSummary(output: string): { passed: number; failed: number } | undefined {
|
|
129
|
+
let passed: number | undefined;
|
|
130
|
+
let failed: number | undefined;
|
|
131
|
+
const set = (p?: string, f?: string) => {
|
|
132
|
+
if (p !== undefined) passed = (passed ?? 0) + Number(p);
|
|
133
|
+
if (f !== undefined) failed = (failed ?? 0) + Number(f);
|
|
134
|
+
};
|
|
135
|
+
for (const line of output.split("\n")) {
|
|
136
|
+
const clean = line.replace(/\x1b\[[0-9;]*m/g, "");
|
|
137
|
+
let m: RegExpMatchArray | null;
|
|
138
|
+
if ((m = clean.match(/^ℹ pass (\d+)/))) set(m[1]);
|
|
139
|
+
else if ((m = clean.match(/^ℹ fail (\d+)/))) set(undefined, m[1]);
|
|
140
|
+
else if ((m = clean.match(/Tests?:?\s+(?:(\d+) failed[,|\s]+)?(\d+) passed/i))) set(m[2], m[1] ?? "0");
|
|
141
|
+
else if ((m = clean.match(/test result: \w+\. (\d+) passed; (\d+) failed/))) set(m[1], m[2]);
|
|
142
|
+
else if ((m = clean.match(/^=+ (?:(\d+) failed, )?(\d+) passed/))) set(m[2], m[1] ?? "0");
|
|
143
|
+
}
|
|
144
|
+
return passed === undefined && failed === undefined ? undefined : { passed: passed ?? 0, failed: failed ?? 0 };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Pretty JSON when the whole output is one JSON value; else undefined. */
|
|
148
|
+
export function asJson(output: string): string | undefined {
|
|
149
|
+
const t = output.trim();
|
|
150
|
+
if (!/^[[{]/.test(t) || t.length > 200_000) return undefined;
|
|
151
|
+
try {
|
|
152
|
+
return JSON.stringify(JSON.parse(t), null, 2);
|
|
153
|
+
} catch {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — the pi settings that shape the built-in tools
|
|
3
|
+
*
|
|
4
|
+
* pi builds read/bash with `images.autoResize`, `shellCommandPrefix` and
|
|
5
|
+
* `shellPath`. Re-registered tools must behave identically, so the same
|
|
6
|
+
* values are read here (global ~/.pi/agent/settings.json, then the project's
|
|
7
|
+
* .pi/settings.json on top).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
function readJson(file: string): Record<string, unknown> {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(file, "utf-8")) as Record<string, unknown>;
|
|
17
|
+
} catch {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PiToolOptions {
|
|
23
|
+
read: { autoResizeImages?: boolean };
|
|
24
|
+
bash: { commandPrefix?: string; shellPath?: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function readPiToolOptions(cwd: string, agentDir = process.env.PI_AGENT_DIR || join(homedir(), ".pi", "agent")): PiToolOptions {
|
|
28
|
+
const merged = { ...readJson(join(agentDir, "settings.json")), ...readJson(join(cwd, ".pi", "settings.json")) };
|
|
29
|
+
const images = merged.images as { autoResize?: unknown } | undefined;
|
|
30
|
+
const shellPath = typeof merged.shellPath === "string" ? merged.shellPath.replace(/^~(?=\/|$)/, homedir()) : undefined;
|
|
31
|
+
return {
|
|
32
|
+
read: typeof images?.autoResize === "boolean" ? { autoResizeImages: images.autoResize } : {},
|
|
33
|
+
bash: {
|
|
34
|
+
...(typeof merged.shellCommandPrefix === "string" ? { commandPrefix: merged.shellCommandPrefix } : {}),
|
|
35
|
+
...(shellPath ? { shellPath } : {}),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — "simple" style: the final reply gets a soft background.
|
|
3
|
+
*
|
|
4
|
+
* In a long transcript of collapsed tool rows the actual answer is easy to
|
|
5
|
+
* miss. An assistant message with no tool calls (the turn's reply) is painted
|
|
6
|
+
* on the darkest possible panel (true black), framed by an orange heavy rule
|
|
7
|
+
* labelled "summary" above and a matching unlabelled rule below, with one blank
|
|
8
|
+
* row of breathing space before the top rule. On light themes the text is lifted to
|
|
9
|
+
* bright white so it stays readable on the black panel.
|
|
10
|
+
*
|
|
11
|
+
* pi has no hook for assistant-message rendering, and extensions get a
|
|
12
|
+
* different module instance of AssistantMessageComponent than the one pi
|
|
13
|
+
* renders, so the class is found in the live TUI tree (duck-typed) and its
|
|
14
|
+
* render is wrapped once. Output is cached per component by width + message +
|
|
15
|
+
* colour, so a keystroke re-render is a lookup, not a repaint.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
19
|
+
|
|
20
|
+
const PROBE_WIDGET = "unipi-reply-bg-probe";
|
|
21
|
+
const PATCHED = Symbol.for("unipi.replyBg.patched");
|
|
22
|
+
|
|
23
|
+
interface AssistantLike {
|
|
24
|
+
hasToolCalls: boolean;
|
|
25
|
+
isStreaming: boolean;
|
|
26
|
+
lastMessage?: unknown;
|
|
27
|
+
contentContainer: unknown;
|
|
28
|
+
updateContent: (...args: unknown[]) => void;
|
|
29
|
+
render: (width: number) => string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface TreeNode {
|
|
33
|
+
children?: unknown[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function isAssistant(c: unknown): c is AssistantLike {
|
|
37
|
+
const a = c as Partial<AssistantLike> | null;
|
|
38
|
+
return !!a && typeof a === "object" && "contentContainer" in a && "hasToolCalls" in a && typeof a.updateContent === "function";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Depth-first search of the TUI tree for pi's assistant message component. */
|
|
42
|
+
export function findAssistant(root: unknown, depth = 0): AssistantLike | undefined {
|
|
43
|
+
if (!root || typeof root !== "object" || depth > 12) return undefined;
|
|
44
|
+
if (isAssistant(root)) return root;
|
|
45
|
+
for (const child of (root as TreeNode).children ?? []) {
|
|
46
|
+
const hit = findAssistant(child, depth + 1);
|
|
47
|
+
if (hit) return hit;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Relative luminance 0..1 of an ANSI background escape (truecolor or 256). */
|
|
53
|
+
export function bgLuminance(ansi: string): number | undefined {
|
|
54
|
+
const tc = /48;2;(\d+);(\d+);(\d+)/.exec(ansi);
|
|
55
|
+
if (tc) return (0.2126 * +tc[1]! + 0.7152 * +tc[2]! + 0.0722 * +tc[3]!) / 255;
|
|
56
|
+
const c256 = /48;5;(\d+)/.exec(ansi);
|
|
57
|
+
if (c256) {
|
|
58
|
+
const n = +c256[1]!;
|
|
59
|
+
if (n >= 232) return (8 + (n - 232) * 10) / 255;
|
|
60
|
+
if (n >= 16) {
|
|
61
|
+
const i = n - 16;
|
|
62
|
+
const v = (x: number) => (x === 0 ? 0 : 55 + x * 40);
|
|
63
|
+
return (0.2126 * v(Math.floor(i / 36)) + 0.7152 * v(Math.floor(i / 6) % 6) + 0.0722 * v(i % 6)) / 255;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The reply background: the darkest possible (true black). */
|
|
70
|
+
export function replyBg(_theme?: unknown): string {
|
|
71
|
+
return "\x1b[48;2;0;0;0m";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Bright white fg re-opened after resets when the theme is light (black panel). */
|
|
75
|
+
export function replyFg(theme: { getBgAnsi?: (k: string) => string } | undefined): string | null {
|
|
76
|
+
try {
|
|
77
|
+
const lum = bgLuminance(theme?.getBgAnsi?.("userMessageBg") ?? "");
|
|
78
|
+
return lum !== undefined && lum > 0.5 ? "\x1b[97m" : null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The rule colour: orange, truecolor on capable terminals, 256-colour fallback. */
|
|
85
|
+
export function ruleFg(theme: { getColorMode?: () => string } | undefined): string {
|
|
86
|
+
let mode = "truecolor";
|
|
87
|
+
try {
|
|
88
|
+
mode = theme?.getColorMode?.() ?? "truecolor";
|
|
89
|
+
} catch {}
|
|
90
|
+
return mode === "truecolor" ? "\x1b[38;2;255;135;0m" : "\x1b[38;5;208m";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The heavy rule that frames the panel. With a label the word sits centred
|
|
95
|
+
* between two runs of rule ("━━━━━ summary ━━━━━"); without one the rule
|
|
96
|
+
* spans the full width (the closing edge under the panel). Painted in orange
|
|
97
|
+
* with a bold label.
|
|
98
|
+
*/
|
|
99
|
+
export function dividerLine(
|
|
100
|
+
theme: { getColorMode?: () => string; bold?: (t: string) => string } | undefined,
|
|
101
|
+
label: string,
|
|
102
|
+
width: number,
|
|
103
|
+
): string {
|
|
104
|
+
const w = Math.max(0, width);
|
|
105
|
+
const rule = "━";
|
|
106
|
+
const color = (t: string) => `${ruleFg(theme)}${t}\x1b[39m`;
|
|
107
|
+
const bold = (t: string) => {
|
|
108
|
+
try {
|
|
109
|
+
return theme?.bold?.(t) ?? `\x1b[1m${t}\x1b[22m`;
|
|
110
|
+
} catch {
|
|
111
|
+
return `\x1b[1m${t}\x1b[22m`;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
if (!label) return color(rule.repeat(w));
|
|
115
|
+
const text = ` ${label} `;
|
|
116
|
+
const inner = visibleWidth(text);
|
|
117
|
+
if (inner >= w) return color(text.slice(0, w));
|
|
118
|
+
const left = Math.floor((w - inner) / 2);
|
|
119
|
+
return color(rule.repeat(left)) + bold(color(text)) + color(rule.repeat(w - inner - left));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Paint one line on `bg`, padded to `width`. Inner resets (`\x1b[0m`,
|
|
124
|
+
* `\x1b[49m`) would end the background mid-line, so the background is
|
|
125
|
+
* re-opened right after each of them.
|
|
126
|
+
*/
|
|
127
|
+
export function paintLine(line: string, width: number, bg: string, fg: string | null = null): string {
|
|
128
|
+
const pad = Math.max(0, width - visibleWidth(line));
|
|
129
|
+
const reopen = (m: string) => m + bg + (fg ?? "");
|
|
130
|
+
const body = line.replace(/\x1b\[(?:0|39|49)m/g, reopen);
|
|
131
|
+
return `${bg}${fg ?? ""}${body}${" ".repeat(pad)}\x1b[49m${fg ? "\x1b[39m" : ""}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A step with no visible text (thinking and/or tool calls only) that ended normally. */
|
|
135
|
+
export function isBlankStep(msg: unknown): boolean {
|
|
136
|
+
const m = msg as { content?: unknown; stopReason?: string } | undefined;
|
|
137
|
+
if (!m || !Array.isArray(m.content)) return false;
|
|
138
|
+
if (m.stopReason === "error" || m.stopReason === "aborted" || m.stopReason === "length") return false;
|
|
139
|
+
return !m.content.some((c: any) => c?.type === "text" && typeof c.text === "string" && c.text.trim());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** A rendered row that paints nothing: whitespace + SGR/OSC sequences only. */
|
|
143
|
+
const OSC_SEQ = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
|
144
|
+
const SGR_SEQ = /\x1b\[[0-9;]*m/g;
|
|
145
|
+
|
|
146
|
+
export function isBlankRendered(line: string): boolean {
|
|
147
|
+
return line.replace(OSC_SEQ, "").replace(SGR_SEQ, "").trim() === "";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Drop rendered blank rows at both edges of a component's output. pi parks a
|
|
152
|
+
* Spacer(1) as the first child of every content-bearing assistant message —
|
|
153
|
+
* rendered, it's a stray blank row below custom badges and above mid-turn
|
|
154
|
+
* text, and a pad row under the summary rule inside the reply panel. Dropped
|
|
155
|
+
* edge lines keep their OSC-133 zone markers (pi prepends `\x1b]133;A\x07` to
|
|
156
|
+
* the first line of a tool-free message): they're moved onto the surviving
|
|
157
|
+
* edge lines so prompt-zone navigation isn't lost.
|
|
158
|
+
*/
|
|
159
|
+
export function trimEdgeBlankLines(lines: string[]): string[] {
|
|
160
|
+
let start = 0;
|
|
161
|
+
let end = lines.length;
|
|
162
|
+
while (start < end && isBlankRendered(lines[start]!)) start++;
|
|
163
|
+
while (end > start && isBlankRendered(lines[end - 1]!)) end--;
|
|
164
|
+
const kept = lines.slice(start, end);
|
|
165
|
+
if (kept.length === 0) return kept;
|
|
166
|
+
const zones = (ls: string[]) => ls.join("").match(OSC_SEQ)?.join("") ?? "";
|
|
167
|
+
// Tail first, then head: a single surviving line ends up head+tail+text,
|
|
168
|
+
// matching pi's A…B C order (zone start precedes zone end).
|
|
169
|
+
const tail = zones(lines.slice(end));
|
|
170
|
+
if (tail) kept[kept.length - 1] = tail + kept[kept.length - 1];
|
|
171
|
+
const head = zones(lines.slice(0, start));
|
|
172
|
+
if (head) kept[0] = head + kept[0];
|
|
173
|
+
return kept;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Drop children of a message's content container that render only blank rows.
|
|
178
|
+
* In simple mode that's each hidden-thinking run: pi wraps the label Text in a
|
|
179
|
+
* MouseRegion, and theme.italic(theme.fg(key, "")) is ANSI escapes around an
|
|
180
|
+
* empty string — it survives Text's empty check (`trim()` keeps escapes) and
|
|
181
|
+
* paints a full blank row — plus the Spacer pi parks after a thinking run. A
|
|
182
|
+
* thinking+text step would otherwise show ~3 blank rows above the text. A
|
|
183
|
+
* region with real content (non-empty label) is kept.
|
|
184
|
+
*/
|
|
185
|
+
export function stripBlankRuns(container: unknown, width: number): void {
|
|
186
|
+
const kids = (container as TreeNode | undefined)?.children;
|
|
187
|
+
if (!Array.isArray(kids)) return;
|
|
188
|
+
const blank = (l: string) => l.replace(/\x1b\[[0-9;]*m/g, "").trim() === "";
|
|
189
|
+
for (let i = kids.length - 1; i >= 0; i--) {
|
|
190
|
+
const child = kids[i] as { child?: unknown; render?: (w: number) => string[] } | null;
|
|
191
|
+
if (!child || typeof child !== "object" || !("child" in child) || typeof child.render !== "function") continue;
|
|
192
|
+
let empty = false;
|
|
193
|
+
try {
|
|
194
|
+
empty = child.render(width).every(blank);
|
|
195
|
+
} catch {}
|
|
196
|
+
if (!empty) continue;
|
|
197
|
+
kids.splice(i, 1);
|
|
198
|
+
const next = kids[i] as { lines?: unknown } | null | undefined;
|
|
199
|
+
if (next && typeof next === "object" && typeof next.lines === "number") kids.splice(i, 1);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Wrap the component class's render once. `getBg` is read per render so a
|
|
205
|
+
* theme switch takes effect; `enabled` lets the caller turn it off.
|
|
206
|
+
*/
|
|
207
|
+
export function patchAssistantRender(
|
|
208
|
+
proto: AssistantLike,
|
|
209
|
+
getPanel: () => { bg: string; fg: string | null; dividerFor: (width: number, label?: string) => string },
|
|
210
|
+
): void {
|
|
211
|
+
const p = proto as unknown as Record<PropertyKey, unknown>;
|
|
212
|
+
if (p[PATCHED]) return;
|
|
213
|
+
const original = proto.render;
|
|
214
|
+
const cache = new WeakMap<object, { width: number; msg: unknown; bg: string; streaming: boolean; lines: string[] }>();
|
|
215
|
+
proto.render = function (this: AssistantLike, width: number): string[] {
|
|
216
|
+
// Simple mode hides thinking, but pi still adds a leading Spacer for a
|
|
217
|
+
// thinking-only step (and a hidden label line), so every tool step left
|
|
218
|
+
// blank rows — a growing gap above tool groups. No visible text → no rows.
|
|
219
|
+
if (isBlankStep(this.lastMessage)) return [];
|
|
220
|
+
stripBlankRuns(this.contentContainer, width);
|
|
221
|
+
const lines = trimEdgeBlankLines(original.call(this, width));
|
|
222
|
+
// Only the reply: finished, no tool calls, something visible — and the
|
|
223
|
+
// message must have ended cleanly. An error/abort/length/deferred tail
|
|
224
|
+
// isn't the turn's reply and doesn't get the summary frame.
|
|
225
|
+
if (this.hasToolCalls || this.isStreaming || lines.length === 0) return lines;
|
|
226
|
+
const stop = (this.lastMessage as { stopReason?: string } | undefined)?.stopReason;
|
|
227
|
+
if (stop === "pending" || stop === "error" || stop === "aborted" || stop === "length" || stop === "deferred") return lines;
|
|
228
|
+
const { bg, fg, dividerFor } = getPanel();
|
|
229
|
+
const hit = cache.get(this);
|
|
230
|
+
if (hit && hit.width === width && hit.msg === this.lastMessage && hit.bg === bg && hit.streaming === this.isStreaming) return hit.lines;
|
|
231
|
+
const painted = [
|
|
232
|
+
"", // breathing space above the section rule
|
|
233
|
+
dividerFor(width),
|
|
234
|
+
// Exactly one pad row below the rule, mirroring the bottom pad: the
|
|
235
|
+
// reply (its `●` anchor) starts one blank row under "─ summary ─" —
|
|
236
|
+
// never flush, never two.
|
|
237
|
+
paintLine("", width, bg),
|
|
238
|
+
...lines.map((l) => paintLine(l, width, bg, fg)),
|
|
239
|
+
// A bottom pad row so the panel doesn't end flush on the last text line.
|
|
240
|
+
paintLine("", width, bg),
|
|
241
|
+
dividerFor(width, ""), // closing edge under the panel
|
|
242
|
+
];
|
|
243
|
+
cache.set(this, { width, msg: this.lastMessage, bg, streaming: this.isStreaming, lines: painted });
|
|
244
|
+
return painted;
|
|
245
|
+
};
|
|
246
|
+
p[PATCHED] = true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Install for the session: grab the TUI via an empty widget, and after each
|
|
251
|
+
* agent turn look for an assistant component until one is found and patched.
|
|
252
|
+
* `onTick` runs on every attempt once the TUI is known — it gets the tree root,
|
|
253
|
+
* which the simple renderer uses to reconcile tool-call grouping after a
|
|
254
|
+
* resumed-session history render (replay fires no message_* events, so the
|
|
255
|
+
* event-driven group breaks never happen and every call lands in one group).
|
|
256
|
+
*/
|
|
257
|
+
export function installReplyBackground(pi: {
|
|
258
|
+
on: (event: any, handler: (event: any, ctx?: any) => void) => void;
|
|
259
|
+
}, onTick?: (tui: unknown) => void): void {
|
|
260
|
+
let tui: unknown;
|
|
261
|
+
let theme: any;
|
|
262
|
+
let done = false;
|
|
263
|
+
const tryPatch = () => {
|
|
264
|
+
if (tui) {
|
|
265
|
+
try {
|
|
266
|
+
onTick?.(tui);
|
|
267
|
+
} catch {}
|
|
268
|
+
}
|
|
269
|
+
if (done || !tui) return;
|
|
270
|
+
const hit = findAssistant(tui);
|
|
271
|
+
if (!hit) return;
|
|
272
|
+
patchAssistantRender(Object.getPrototypeOf(hit) as AssistantLike, () => ({
|
|
273
|
+
bg: replyBg(),
|
|
274
|
+
fg: replyFg(theme),
|
|
275
|
+
dividerFor: (width: number, label = "summary") => dividerLine(theme, label, width),
|
|
276
|
+
}));
|
|
277
|
+
done = true;
|
|
278
|
+
try {
|
|
279
|
+
(tui as { requestRender?: (force?: boolean) => void }).requestRender?.(true);
|
|
280
|
+
} catch {}
|
|
281
|
+
};
|
|
282
|
+
try {
|
|
283
|
+
pi.on("session_start", (_e: any, ctx: any) => {
|
|
284
|
+
if (!ctx?.hasUI) return;
|
|
285
|
+
try {
|
|
286
|
+
ctx.ui.setWidget(
|
|
287
|
+
PROBE_WIDGET,
|
|
288
|
+
(t: unknown, th: unknown) => {
|
|
289
|
+
tui = t;
|
|
290
|
+
theme = th;
|
|
291
|
+
return { invalidate() {}, render: () => [] };
|
|
292
|
+
},
|
|
293
|
+
{ placement: "belowEditor" },
|
|
294
|
+
);
|
|
295
|
+
// Resumed sessions already have replies on screen; history renders
|
|
296
|
+
// right after extension init, so probe a few times until the tree is
|
|
297
|
+
// populated even if the user never sends another message.
|
|
298
|
+
for (const ms of [0, 400, 2000]) setTimeout(tryPatch, ms);
|
|
299
|
+
} catch {}
|
|
300
|
+
});
|
|
301
|
+
pi.on("message_start", () => setTimeout(tryPatch, 0));
|
|
302
|
+
pi.on("message_end", () => setTimeout(tryPatch, 0));
|
|
303
|
+
pi.on("agent_end", () => setTimeout(tryPatch, 0));
|
|
304
|
+
} catch {
|
|
305
|
+
// cosmetic; never block load
|
|
306
|
+
}
|
|
307
|
+
}
|