@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,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — Automatic session naming
|
|
3
|
+
*
|
|
4
|
+
* After every confirmed round (agent_end without error/abort) whose prompt the
|
|
5
|
+
* user typed: gate (prefilter + jev) → isolated one-tool rename session →
|
|
6
|
+
* pi.setSessionName. Names the user sets (pi's /name) are respected: once the
|
|
7
|
+
* current name differs from the last auto name, auto-rename stops for that
|
|
8
|
+
* session. "Rename now" in /unipi:settings bypasses the gate.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { appendFileSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { askJev, registerCommandRunner, resolveDecisionModel } from "@pi-unipi/core";
|
|
16
|
+
import { readRenameSettings } from "../settings.js";
|
|
17
|
+
import { detectHerdr, syncPaneTitle, type HerdrEnv } from "../herdr-sync.js";
|
|
18
|
+
import { decide, gateRequest, isIdleRound } from "./gate.js";
|
|
19
|
+
import { runRenameSession } from "./session.js";
|
|
20
|
+
|
|
21
|
+
/** Custom entry recording names auto-rename set (so resume can tell them from /name). */
|
|
22
|
+
export const AUTO_NAME_ENTRY = "unipi:auto-name";
|
|
23
|
+
/** Rounds that must pass between two automatic renames (never twice in one round). */
|
|
24
|
+
const MIN_ROUNDS_BETWEEN = 1;
|
|
25
|
+
const HERDR_POLL_MS = 3000;
|
|
26
|
+
|
|
27
|
+
/** Debug trail, gated by UNIPI_DEBUG_RENAME=1 → ~/.unipi/logs/rename.log. */
|
|
28
|
+
function debugLog(line: string): void {
|
|
29
|
+
if (process.env.UNIPI_DEBUG_RENAME !== "1") return;
|
|
30
|
+
try {
|
|
31
|
+
const dir = join(homedir(), ".unipi", "logs");
|
|
32
|
+
mkdirSync(dir, { recursive: true });
|
|
33
|
+
appendFileSync(join(dir, "rename.log"), `${new Date().toISOString()} ${line}\n`);
|
|
34
|
+
} catch {
|
|
35
|
+
// best effort
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Text of the last assistant message of a round (what the agent concluded). */
|
|
40
|
+
export function lastReplyText(messages: readonly unknown[]): string {
|
|
41
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
42
|
+
const m = messages[i] as { role?: string; content?: unknown };
|
|
43
|
+
if (m.role !== "assistant") continue;
|
|
44
|
+
const text = typeof m.content === "string"
|
|
45
|
+
? m.content
|
|
46
|
+
: Array.isArray(m.content) ? m.content.filter((c: { type?: string }) => c?.type === "text").map((c: { text?: string }) => c.text ?? "").join("\n") : "";
|
|
47
|
+
if (text.trim()) return text.replace(/<\/?summary>/g, "").trim().slice(0, 1500);
|
|
48
|
+
}
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function countToolCalls(messages: readonly unknown[]): number {
|
|
53
|
+
let n = 0;
|
|
54
|
+
for (const m of messages as Array<{ role?: string; content?: unknown }>) {
|
|
55
|
+
if (m.role === "assistant" && Array.isArray(m.content)) n += m.content.filter((c: { type?: string }) => c?.type === "toolCall").length;
|
|
56
|
+
}
|
|
57
|
+
return n;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function registerAutoRename(pi: ExtensionAPI): void {
|
|
61
|
+
let requests: string[] = [];
|
|
62
|
+
/** The round that triggered the current rename (the name should describe it). */
|
|
63
|
+
let lastRound: { reply: string; toolCalls: number } = { reply: "", toolCalls: 0 };
|
|
64
|
+
let pending: string | null = null;
|
|
65
|
+
let lastAutoName: string | null = null;
|
|
66
|
+
let roundsSinceRename = Number.POSITIVE_INFINITY;
|
|
67
|
+
let inFlight = false;
|
|
68
|
+
let herdr: HerdrEnv = { enabled: false };
|
|
69
|
+
let herdrShown: string | null = null;
|
|
70
|
+
let herdrTimer: ReturnType<typeof setInterval> | null = null;
|
|
71
|
+
|
|
72
|
+
const currentName = (): string | null => {
|
|
73
|
+
try {
|
|
74
|
+
return pi.getSessionName() ?? null;
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const syncHerdr = (name: string | null): void => {
|
|
81
|
+
if (!herdr.enabled || name === herdrShown || !readRenameSettings().herdrSync) return;
|
|
82
|
+
herdrShown = name;
|
|
83
|
+
void syncPaneTitle(herdr, name);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const apply = (name: string): void => {
|
|
87
|
+
if (name === currentName()) return;
|
|
88
|
+
pi.setSessionName(name);
|
|
89
|
+
lastAutoName = name;
|
|
90
|
+
roundsSinceRename = 0;
|
|
91
|
+
pi.appendEntry(AUTO_NAME_ENTRY, { name });
|
|
92
|
+
syncHerdr(name);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const rename = async (ctx: ExtensionContext, force: boolean, topicChanged = false): Promise<string | undefined> => {
|
|
96
|
+
if (inFlight) return undefined;
|
|
97
|
+
inFlight = true;
|
|
98
|
+
try {
|
|
99
|
+
const settings = readRenameSettings(ctx.cwd);
|
|
100
|
+
const name = await runRenameSession(ctx, { currentName: currentName(), requests, model: settings.model, topicChanged, reply: lastRound.reply }, debugLog);
|
|
101
|
+
debugLog(`rename session → ${name === undefined ? "(no name)" : JSON.stringify(name)}`);
|
|
102
|
+
if (name && (force || name !== currentName())) apply(name);
|
|
103
|
+
return name;
|
|
104
|
+
} catch (error) {
|
|
105
|
+
debugLog(`rename session failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
106
|
+
return undefined;
|
|
107
|
+
} finally {
|
|
108
|
+
inFlight = false;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
registerCommandRunner("unipi:rename-now", async (raw: unknown) => {
|
|
113
|
+
const ctx = raw as ExtensionContext;
|
|
114
|
+
if (requests.length === 0) {
|
|
115
|
+
ctx.ui?.notify?.("Nothing to name yet — send a request first.", "info");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const name = await rename(ctx, true);
|
|
119
|
+
ctx.ui?.notify?.(name ? `Session named "${name}"` : "Could not generate a name.", name ? "info" : "warning");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
pi.on("session_start", (_event, ctx) => {
|
|
123
|
+
try {
|
|
124
|
+
requests = [];
|
|
125
|
+
pending = null;
|
|
126
|
+
roundsSinceRename = Number.POSITIVE_INFINITY;
|
|
127
|
+
const entries = (ctx.sessionManager?.getEntries?.() ?? []) as Array<{ type?: string; customType?: string; data?: { name?: unknown } }>;
|
|
128
|
+
const last = entries.findLast((e) => e.type === "custom" && e.customType === AUTO_NAME_ENTRY);
|
|
129
|
+
lastAutoName = typeof last?.data?.name === "string" ? last.data.name : null;
|
|
130
|
+
herdr = detectHerdr();
|
|
131
|
+
herdrShown = null;
|
|
132
|
+
syncHerdr(currentName());
|
|
133
|
+
if (herdrTimer) clearInterval(herdrTimer);
|
|
134
|
+
herdrTimer = herdr.enabled ? setInterval(() => syncHerdr(currentName()), HERDR_POLL_MS) : null;
|
|
135
|
+
herdrTimer?.unref?.();
|
|
136
|
+
} catch {
|
|
137
|
+
// Naming is best-effort — never block session start.
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
pi.on("input", (event) => {
|
|
142
|
+
try {
|
|
143
|
+
if (event.source === "extension") return;
|
|
144
|
+
const text = (event.text ?? "").trim();
|
|
145
|
+
if (!text || text.startsWith("/")) return;
|
|
146
|
+
requests = [...requests, text].slice(-6);
|
|
147
|
+
pending = text;
|
|
148
|
+
} catch {
|
|
149
|
+
// ignore
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
pi.on("agent_end", (event, ctx) => {
|
|
154
|
+
const prompt = pending;
|
|
155
|
+
pending = null;
|
|
156
|
+
roundsSinceRename++;
|
|
157
|
+
try {
|
|
158
|
+
if (!prompt || inFlight) return;
|
|
159
|
+
const skip = (why: string) => debugLog(`skip ${JSON.stringify(prompt.slice(0, 60))}: ${why}`);
|
|
160
|
+
if (!readRenameSettings(ctx.cwd).auto) return skip("auto-rename off");
|
|
161
|
+
const lastAssistant = [...(event.messages ?? [])].reverse().find((m) => (m as { role?: string }).role === "assistant") as { stopReason?: string } | undefined;
|
|
162
|
+
if (lastAssistant?.stopReason === "error" || lastAssistant?.stopReason === "aborted") return skip(`round ${lastAssistant.stopReason}`);
|
|
163
|
+
const name = currentName();
|
|
164
|
+
if (name && name !== lastAutoName) return skip("user-set name"); // user-set name wins
|
|
165
|
+
if (name && roundsSinceRename < MIN_ROUNDS_BETWEEN) return skip("renamed recently");
|
|
166
|
+
const reply = lastReplyText(event.messages ?? []);
|
|
167
|
+
const toolCalls = countToolCalls(event.messages ?? []);
|
|
168
|
+
const input = { prompt, currentName: name, earlier: requests, reply, toolCalls };
|
|
169
|
+
if (isIdleRound(input)) return skip("chatter");
|
|
170
|
+
lastRound = { reply, toolCalls };
|
|
171
|
+
// Detached: the next prompt never waits on naming.
|
|
172
|
+
void (async () => {
|
|
173
|
+
try {
|
|
174
|
+
const answers = await askJev({ ...gateRequest(input), settings: resolveDecisionModel(ctx.cwd ?? process.cwd(), "utility"), env: process.env });
|
|
175
|
+
const decision = decide(input, answers);
|
|
176
|
+
debugLog(`gate ${JSON.stringify(prompt.slice(0, 60))} name=${JSON.stringify(name)} → ${decision.rename ? "rename" : "skip"} (${decision.reason})`);
|
|
177
|
+
if (decision.rename) await rename(ctx, false, Boolean(name));
|
|
178
|
+
} catch {
|
|
179
|
+
// Naming must never disturb the main session.
|
|
180
|
+
}
|
|
181
|
+
})();
|
|
182
|
+
} catch {
|
|
183
|
+
// ignore
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
pi.on("session_shutdown", () => {
|
|
188
|
+
if (herdrTimer) clearInterval(herdrTimer);
|
|
189
|
+
herdrTimer = null;
|
|
190
|
+
});
|
|
191
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — Rename session
|
|
3
|
+
*
|
|
4
|
+
* A throwaway in-memory pi session (the /unipi:btw shape) whose ONLY tool is
|
|
5
|
+
* `rename_session`. It never loads extensions, skills or context files, never
|
|
6
|
+
* sees the main transcript, and nothing it does reaches the main session
|
|
7
|
+
* except the name it sets. The main agent never has a rename tool.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
createAgentSession,
|
|
12
|
+
createExtensionRuntime,
|
|
13
|
+
defineTool,
|
|
14
|
+
SessionManager,
|
|
15
|
+
type ModelRuntime,
|
|
16
|
+
type ResourceLoader,
|
|
17
|
+
} from "@earendil-works/pi-coding-agent";
|
|
18
|
+
import { Type } from "typebox";
|
|
19
|
+
import { sanitizeName } from "./gate.js";
|
|
20
|
+
|
|
21
|
+
const SYSTEM_PROMPT = [
|
|
22
|
+
"You name coding-agent sessions so the user can find them later.",
|
|
23
|
+
"Call rename_session exactly once with a short title (2-6 words, Title Case) describing the user's current task.",
|
|
24
|
+
"Name the task, not the conversation: 'Kanboard Lane Ordering', not 'User Asks About Lanes'.",
|
|
25
|
+
"If the current title still describes the current task, call rename_session with the current title unchanged.",
|
|
26
|
+
"Do not write anything else.",
|
|
27
|
+
].join("\n");
|
|
28
|
+
|
|
29
|
+
const RENAME_TIMEOUT_MS = 45_000;
|
|
30
|
+
|
|
31
|
+
export interface RenameCtx {
|
|
32
|
+
cwd?: string;
|
|
33
|
+
model?: unknown;
|
|
34
|
+
modelRegistry?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RenameRequest {
|
|
38
|
+
currentName: string | null;
|
|
39
|
+
requests: readonly string[];
|
|
40
|
+
/** "" = session model; otherwise "provider/model-id". */
|
|
41
|
+
model: string;
|
|
42
|
+
/** The gate found the latest request leaves the current title's topic. */
|
|
43
|
+
topicChanged?: boolean;
|
|
44
|
+
/** The agent's reply in the triggering round — often the clearest subject. */
|
|
45
|
+
reply?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resourceLoader(): ResourceLoader {
|
|
49
|
+
const extensions = { extensions: [], errors: [], runtime: createExtensionRuntime() };
|
|
50
|
+
return {
|
|
51
|
+
getExtensions: () => extensions,
|
|
52
|
+
getSkills: () => ({ skills: [], diagnostics: [] }),
|
|
53
|
+
getPrompts: () => ({ prompts: [], diagnostics: [] }),
|
|
54
|
+
getThemes: () => ({ themes: [], diagnostics: [] }),
|
|
55
|
+
getAgentsFiles: () => ({ agentsFiles: [] }),
|
|
56
|
+
getSystemPrompt: () => SYSTEM_PROMPT,
|
|
57
|
+
getSystemPromptSource: () => undefined,
|
|
58
|
+
getAppendSystemPrompt: () => [],
|
|
59
|
+
getAppendSystemPromptSources: () => [],
|
|
60
|
+
extendResources: () => {},
|
|
61
|
+
reload: async () => {},
|
|
62
|
+
} as unknown as ResourceLoader;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveModel(ref: string, ctx: RenameCtx): unknown {
|
|
66
|
+
const registry = ctx.modelRegistry as { find?: (p: string, id: string) => unknown } | undefined;
|
|
67
|
+
const slash = ref.indexOf("/");
|
|
68
|
+
if (ref && slash > 0 && registry?.find) {
|
|
69
|
+
const found = registry.find(ref.slice(0, slash), ref.slice(slash + 1));
|
|
70
|
+
if (found) return found;
|
|
71
|
+
}
|
|
72
|
+
return ctx.model;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function renamePrompt(req: RenameRequest): string {
|
|
76
|
+
const clip = (r: string) => r.replace(/\s+/g, " ").slice(0, 400);
|
|
77
|
+
if (req.topicChanged && req.currentName) {
|
|
78
|
+
const latest = req.requests.at(-1) ?? "";
|
|
79
|
+
return [
|
|
80
|
+
`Current title: ${req.currentName}`,
|
|
81
|
+
"",
|
|
82
|
+
"The user has moved on to a different task, so the current title no longer fits. Name the NEW task:",
|
|
83
|
+
`- ${clip(latest)}`,
|
|
84
|
+
...(req.reply ? ["", `What the assistant did about it:\n${clip(req.reply)}`] : []),
|
|
85
|
+
].join("\n");
|
|
86
|
+
}
|
|
87
|
+
const recent = req.requests.slice(-4).map((r) => `- ${clip(r)}`).join("\n");
|
|
88
|
+
return [
|
|
89
|
+
req.currentName ? `Current title: ${req.currentName}` : "The session has no title yet.",
|
|
90
|
+
"",
|
|
91
|
+
"Recent user requests (oldest first; the last one is the current task):",
|
|
92
|
+
recent || "- (none)",
|
|
93
|
+
...(req.reply ? ["", `The assistant's latest reply (use it when the request itself is vague):\n${clip(req.reply)}`] : []),
|
|
94
|
+
].join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Run the one-tool session; resolves to the sanitized name it chose, if any.
|
|
98
|
+
* A configured naming model that fails (removed from the catalog, no key)
|
|
99
|
+
* falls back once to the session model. */
|
|
100
|
+
export async function runRenameSession(
|
|
101
|
+
ctx: RenameCtx,
|
|
102
|
+
req: RenameRequest,
|
|
103
|
+
debug: (line: string) => void = () => {},
|
|
104
|
+
): Promise<string | undefined> {
|
|
105
|
+
const runtime = (ctx.modelRegistry as { runtime?: ModelRuntime } | undefined)?.runtime;
|
|
106
|
+
if (!runtime) {
|
|
107
|
+
debug("no model runtime available");
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
const configured = resolveModel(req.model, ctx);
|
|
111
|
+
const candidates = configured && configured !== ctx.model ? [configured, ctx.model] : [ctx.model];
|
|
112
|
+
for (const model of candidates) {
|
|
113
|
+
if (!model) continue;
|
|
114
|
+
const name = await attempt(model, runtime, ctx, req, debug);
|
|
115
|
+
if (name) return name;
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function attempt(
|
|
121
|
+
model: unknown,
|
|
122
|
+
runtime: ModelRuntime,
|
|
123
|
+
ctx: RenameCtx,
|
|
124
|
+
req: RenameRequest,
|
|
125
|
+
debug: (line: string) => void,
|
|
126
|
+
): Promise<string | undefined> {
|
|
127
|
+
let chosen: string | undefined;
|
|
128
|
+
const renameTool = defineTool({
|
|
129
|
+
name: "rename_session",
|
|
130
|
+
label: "Rename session",
|
|
131
|
+
description: "Set the session title (2-6 words, Title Case).",
|
|
132
|
+
parameters: Type.Object({ name: Type.String({ description: "The new session title" }) }),
|
|
133
|
+
async execute(_id, params) {
|
|
134
|
+
const name = sanitizeName(String((params as { name?: unknown }).name ?? ""));
|
|
135
|
+
if (!name) return { content: [{ type: "text", text: "Error: empty title." }], details: undefined };
|
|
136
|
+
chosen = name;
|
|
137
|
+
return { content: [{ type: "text", text: `Title set: ${name}` }], details: { name } };
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const { session } = await createAgentSession({
|
|
142
|
+
sessionManager: SessionManager.inMemory(ctx.cwd ?? process.cwd()),
|
|
143
|
+
model: model as never,
|
|
144
|
+
modelRuntime: runtime,
|
|
145
|
+
thinkingLevel: "off" as never,
|
|
146
|
+
tools: ["rename_session"],
|
|
147
|
+
customTools: [renameTool as never],
|
|
148
|
+
resourceLoader: resourceLoader(),
|
|
149
|
+
});
|
|
150
|
+
const s = session as unknown as {
|
|
151
|
+
prompt: (m: string, o?: unknown) => Promise<void>;
|
|
152
|
+
abort?: () => Promise<void> | void;
|
|
153
|
+
dispose?: () => void;
|
|
154
|
+
state?: { messages?: Array<{ role?: string; stopReason?: string; errorMessage?: string; content?: unknown }> };
|
|
155
|
+
};
|
|
156
|
+
const timer = setTimeout(() => void s.abort?.(), RENAME_TIMEOUT_MS);
|
|
157
|
+
timer.unref?.();
|
|
158
|
+
try {
|
|
159
|
+
await s.prompt(renamePrompt(req), { source: "extension" });
|
|
160
|
+
if (!chosen) {
|
|
161
|
+
const last = (s.state?.messages ?? []).findLast((m) => m.role === "assistant");
|
|
162
|
+
const id = (model as { provider?: string; id?: string });
|
|
163
|
+
debug(`${id.provider}/${id.id}: no rename_session call; stop=${last?.stopReason ?? "?"} ${last?.errorMessage ?? JSON.stringify(last?.content ?? "").slice(0, 200)}`);
|
|
164
|
+
}
|
|
165
|
+
} finally {
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
s.dispose?.();
|
|
168
|
+
}
|
|
169
|
+
return chosen;
|
|
170
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @pi-unipi/utility — the approved UNI-2 "delegated work" panel: one outer
|
|
3
|
+
* dark-cyan fill (#12363b) with a bright cyan ▏ rail (#22d3ee) on every row
|
|
4
|
+
* (blank interior rows included), replacing today's nested per-card
|
|
5
|
+
* backgrounds. Rendered content comes from the shared style helpers
|
|
6
|
+
* (styledToolCallLines / styledTextLines / nativeToolComponent for regular
|
|
7
|
+
* raw-args); every nested background SGR is stripped atomically before the
|
|
8
|
+
* single outer fill is applied, so foreground colours (native card fg, syntax,
|
|
9
|
+
* markdown semantics) survive untouched.
|
|
10
|
+
*
|
|
11
|
+
* The returned component carries spacingGroup metadata so adjacent steps join
|
|
12
|
+
* into one continuous panel (spacing.ts resolves it through the host's
|
|
13
|
+
* CustomEntryComponent.customComponent wrapper).
|
|
14
|
+
*/
|
|
15
|
+
import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
16
|
+
import { nativeToolComponent, renderStyle, styledTextLines, styledToolCallLines, type RenderStyle, type StyledTheme } from "./styled.js";
|
|
17
|
+
import { paintLine, trimEdgeBlankLines } from "./reply-bg.js";
|
|
18
|
+
import type { SpacingGroupPosition } from "./spacing.js";
|
|
19
|
+
|
|
20
|
+
/** Minimal single-width component; the cache key (e.g. active render style)
|
|
21
|
+
* invalidates without host help, `invalidate()` clears unconditionally. */
|
|
22
|
+
function styledComponentLike(build: (width: number) => string[], cacheKey: (width: number) => string = () => ""): Component & Record<PropertyKey, unknown> {
|
|
23
|
+
let cache: { width: number; key: string; lines: string[] } | undefined;
|
|
24
|
+
return {
|
|
25
|
+
invalidate(): void {
|
|
26
|
+
cache = undefined;
|
|
27
|
+
},
|
|
28
|
+
render(width: number): string[] {
|
|
29
|
+
const key = cacheKey(width);
|
|
30
|
+
if (cache === undefined || cache.width !== width || cache.key !== key) cache = { width, key, lines: build(width) };
|
|
31
|
+
return cache.lines;
|
|
32
|
+
},
|
|
33
|
+
} as Component & Record<PropertyKey, unknown>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A SidekickStep-compatible step (structural, fusion/subagent agnostic). */
|
|
37
|
+
export type DelegatedStep =
|
|
38
|
+
| { kind: "tool"; name: string; arg: string; output: string; isError: boolean; durationMs: number; args?: Record<string, unknown> }
|
|
39
|
+
| { kind: "text"; text: string; thinking?: string };
|
|
40
|
+
|
|
41
|
+
export interface DelegatedIdentity {
|
|
42
|
+
/** spacingGroup for the panel run — unique per agent/handoff. */
|
|
43
|
+
group: string;
|
|
44
|
+
/** Panel header, drawn once on the first group position (index 0). */
|
|
45
|
+
label?: string;
|
|
46
|
+
/** Render style override; defaults to the active utility render style. */
|
|
47
|
+
style?: RenderStyle;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── approved palette (truecolor, 256-colour fallback) ──────────────────────
|
|
51
|
+
export const DELEGATED_PANEL_HEX = "#12363b";
|
|
52
|
+
export const DELEGATED_RAIL_HEX = "#22d3ee";
|
|
53
|
+
const RAIL = "▏ ";
|
|
54
|
+
|
|
55
|
+
function hexToRgb(hex: string): { r: number; g: number; b: number } {
|
|
56
|
+
const c = hex.replace("#", "");
|
|
57
|
+
return { r: parseInt(c.slice(0, 2), 16), g: parseInt(c.slice(2, 4), 16), b: parseInt(c.slice(4, 6), 16) };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function hexTo256(hex: string): number {
|
|
61
|
+
const { r, g, b } = hexToRgb(hex);
|
|
62
|
+
// Weighted RGB distance (perceptual weights), mirroring pi's own converter:
|
|
63
|
+
// nearest 6×6×6-cube cell vs nearest grayscale ramp entry, closer wins.
|
|
64
|
+
const dist = (r2: number, g2: number, b2: number) => 0.299 * (r - r2) ** 2 + 0.587 * (g - g2) ** 2 + 0.114 * (b - b2) ** 2;
|
|
65
|
+
const cube = [0, 95, 135, 175, 215, 255];
|
|
66
|
+
const nearest = (v: number) => cube.reduce((best, x) => (Math.abs(v - x) < Math.abs(v - best) ? x : best), cube[0]!);
|
|
67
|
+
const ri = cube.indexOf(nearest(r)), gi = cube.indexOf(nearest(g)), bi = cube.indexOf(nearest(b));
|
|
68
|
+
const cubeIndex = 16 + 36 * ri + 6 * gi + bi;
|
|
69
|
+
const cubeDist = dist(cube[ri]!, cube[gi]!, cube[bi]!);
|
|
70
|
+
const gray = Array.from({ length: 24 }, (_, i) => 8 + i * 10);
|
|
71
|
+
let grayBest = 0;
|
|
72
|
+
for (let i = 1; i < gray.length; i++) if (dist(gray[i]!, gray[i]!, gray[i]!) < dist(gray[grayBest]!, gray[grayBest]!, gray[grayBest]!)) grayBest = i;
|
|
73
|
+
const grayIndex = 232 + grayBest;
|
|
74
|
+
const grayDist = dist(gray[grayBest]!, gray[grayBest]!, gray[grayBest]!);
|
|
75
|
+
return cubeDist <= grayDist ? cubeIndex : grayIndex;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type ColorMode = "truecolor" | "256color";
|
|
79
|
+
function modeOf(theme: { getColorMode?: () => ColorMode } | undefined): ColorMode {
|
|
80
|
+
try {
|
|
81
|
+
return theme?.getColorMode?.() ?? "truecolor";
|
|
82
|
+
} catch {
|
|
83
|
+
return "truecolor";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function fgAnsi(hex: string, mode: ColorMode): string {
|
|
88
|
+
if (mode === "256color") return `\x1b[38;5;${String(hexTo256(hex))}m`;
|
|
89
|
+
const { r, g, b } = hexToRgb(hex);
|
|
90
|
+
return `\x1b[38;2;${String(r)};${String(g)};${String(b)}m`;
|
|
91
|
+
}
|
|
92
|
+
function bgAnsi(hex: string, mode: ColorMode): string {
|
|
93
|
+
if (mode === "256color") return `\x1b[48;5;${String(hexTo256(hex))}m`;
|
|
94
|
+
const { r, g, b } = hexToRgb(hex);
|
|
95
|
+
return `\x1b[48;2;${String(r)};${String(g)};${String(b)}m`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The panel fill (dark cyan) in the theme's colour mode. */
|
|
99
|
+
export function delegatedPanelBg(theme?: { getColorMode?: () => ColorMode }): string {
|
|
100
|
+
return bgAnsi(DELEGATED_PANEL_HEX, modeOf(theme));
|
|
101
|
+
}
|
|
102
|
+
/** The rail colour (bright cyan) in the theme's colour mode. */
|
|
103
|
+
export function delegatedRailFg(theme?: { getColorMode?: () => ColorMode }): string {
|
|
104
|
+
return fgAnsi(DELEGATED_RAIL_HEX, modeOf(theme));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Strip every nested background SGR from a rendered line — truecolor, 256 and
|
|
109
|
+
* 8-colour backgrounds plus bg resets — while preserving foreground. Extended
|
|
110
|
+
* sequences (38;2;r;g;b / 38;5;n) are consumed atomically so their colour
|
|
111
|
+
* components can't be mistaken for background params.
|
|
112
|
+
*/
|
|
113
|
+
export function stripNestedBackgrounds(line: string): string {
|
|
114
|
+
return line.replace(/\x1b\[([0-9;]*)m/g, (seq, params: string) => {
|
|
115
|
+
if (params === "") return "\x1b[0m";
|
|
116
|
+
const parts = params.split(";");
|
|
117
|
+
const keep: string[] = [];
|
|
118
|
+
for (let i = 0; i < parts.length; i++) {
|
|
119
|
+
const n = Number(parts[i] || "0");
|
|
120
|
+
if (n === 38 || n === 48) {
|
|
121
|
+
const sub = Number(parts[i + 1]);
|
|
122
|
+
const span = sub === 2 ? 5 : sub === 5 ? 3 : 1;
|
|
123
|
+
const atom = parts.slice(i, i + span).join(";");
|
|
124
|
+
i += span - 1;
|
|
125
|
+
if (n === 38) keep.push(atom);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (n === 49 || (n >= 40 && n <= 47) || (n >= 100 && n <= 107)) continue;
|
|
129
|
+
keep.push(parts[i]!);
|
|
130
|
+
}
|
|
131
|
+
return keep.length > 0 ? `\x1b[${keep.join(";")}m` : "";
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* One rail-painted panel row: cyan ▏ + content on the single outer fill,
|
|
137
|
+
* padded to `width` — blank interior rows get the same treatment, so a panel
|
|
138
|
+
* is continuous with no unpainted gaps. Content backgrounds are stripped.
|
|
139
|
+
*/
|
|
140
|
+
export function paintDelegatedLine(line: string, width: number, theme?: { getColorMode?: () => ColorMode }): string {
|
|
141
|
+
const w = Math.max(1, Math.trunc(width) || 1);
|
|
142
|
+
const painted = paintLine(
|
|
143
|
+
`${delegatedRailFg(theme)}${RAIL}\x1b[39m${stripNestedBackgrounds(line)}`,
|
|
144
|
+
w,
|
|
145
|
+
delegatedPanelBg(theme),
|
|
146
|
+
);
|
|
147
|
+
return truncateToWidth(painted, w);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Semantic fg overrides for delegated panels: cyan accents/titles/results,
|
|
152
|
+
* neutral light output/text, fixed muted metadata and red errors — the user's
|
|
153
|
+
* global theme is never modified. Markdown semantic colours are untouched
|
|
154
|
+
* (they come from the global markdown theme); only these keys are remapped.
|
|
155
|
+
*/
|
|
156
|
+
export function delegatedTheme(theme: StyledTheme, mode: ColorMode = "truecolor"): StyledTheme {
|
|
157
|
+
const overrides = new Map<string, string>([
|
|
158
|
+
["accent", fgAnsi(DELEGATED_RAIL_HEX, mode)],
|
|
159
|
+
["success", fgAnsi(DELEGATED_RAIL_HEX, mode)],
|
|
160
|
+
["toolTitle", fgAnsi(DELEGATED_RAIL_HEX, mode)],
|
|
161
|
+
["toolOutput", fgAnsi("#e8f4f4", mode)],
|
|
162
|
+
["text", fgAnsi("#e8f4f4", mode)],
|
|
163
|
+
["error", fgAnsi("#ff6b6b", mode)],
|
|
164
|
+
["muted", fgAnsi("#86b0b5", mode)],
|
|
165
|
+
["dim", fgAnsi("#5f8a8f", mode)],
|
|
166
|
+
["borderMuted", fgAnsi("#5f8a8f", mode)],
|
|
167
|
+
]);
|
|
168
|
+
return {
|
|
169
|
+
fg(color: string, text: string): string {
|
|
170
|
+
const ansi = overrides.get(color);
|
|
171
|
+
return ansi !== undefined ? `${ansi}${text}\x1b[39m` : theme.fg(color as never, text);
|
|
172
|
+
},
|
|
173
|
+
bold: (t: string) => theme.bold(t),
|
|
174
|
+
} as StyledTheme;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── the step renderer ──────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
/** Test seam: how many times the native component factory actually ran. */
|
|
180
|
+
export const nativeFactoryStats = { calls: 0 };
|
|
181
|
+
|
|
182
|
+
/** Raw native rows for a regular-style step with raw args. The component is
|
|
183
|
+
* cached per (width, expanded, output, error) — the host's rebuild supplies a
|
|
184
|
+
* fresh inner component (and theme) anyway, so within one inner lifetime a
|
|
185
|
+
* repeated render at the same key reuses the native instance instead of
|
|
186
|
+
* re-running the factory. */
|
|
187
|
+
function makeNativeCache() {
|
|
188
|
+
let comp: Component | undefined;
|
|
189
|
+
let key = "";
|
|
190
|
+
return (step: DelegatedStep & { kind: "tool" }, expanded: boolean, inner: number): string[] | undefined => {
|
|
191
|
+
if (step.args === undefined) return undefined;
|
|
192
|
+
const k = `${String(inner)}|${String(expanded)}|${String(step.isError)}|${String(step.output.length)}`;
|
|
193
|
+
if (comp === undefined || key !== k) {
|
|
194
|
+
key = k;
|
|
195
|
+
try {
|
|
196
|
+
nativeFactoryStats.calls++;
|
|
197
|
+
comp = nativeToolComponent({
|
|
198
|
+
name: step.name,
|
|
199
|
+
args: step.args,
|
|
200
|
+
output: step.output,
|
|
201
|
+
isError: step.isError,
|
|
202
|
+
expanded,
|
|
203
|
+
}) ?? undefined;
|
|
204
|
+
} catch {
|
|
205
|
+
comp = undefined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (comp === undefined) return undefined;
|
|
209
|
+
try {
|
|
210
|
+
return trimEdgeBlankLines(comp.render(inner));
|
|
211
|
+
} catch {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* One delegated step as a transcript component in the approved panel design.
|
|
219
|
+
* The component carries `spacingGroup`/`spacingKind`/`setGroupPosition` so
|
|
220
|
+
* spacing.ts can join adjacent members of the same group into one continuous
|
|
221
|
+
* panel: connectors `├` while another tool step follows in the run, `└` on the
|
|
222
|
+
* last; the `label` (when given) is drawn only on group position 0. A fresh
|
|
223
|
+
* native component is cached per inner component instance (keyed by width,
|
|
224
|
+
* expansion, error and output length), so repeated renders reuse it while a
|
|
225
|
+
* theme/expansion rebuild — which creates a fresh inner component — never sees
|
|
226
|
+
* stale state.
|
|
227
|
+
*/
|
|
228
|
+
/** Theme colour mode, read from the passed theme when it exposes one. */
|
|
229
|
+
type ModeTheme = StyledTheme & { getColorMode?: () => "truecolor" | "256color" };
|
|
230
|
+
|
|
231
|
+
export function renderDelegatedStep(step: DelegatedStep, expanded: boolean, theme: StyledTheme, options: DelegatedIdentity): Component & {
|
|
232
|
+
spacingGroup: string;
|
|
233
|
+
spacingKind: string;
|
|
234
|
+
setGroupPosition: (pos: SpacingGroupPosition) => void;
|
|
235
|
+
} {
|
|
236
|
+
const modeTheme = theme as ModeTheme;
|
|
237
|
+
const mode = modeOf(modeTheme);
|
|
238
|
+
const dTheme = delegatedTheme(modeTheme, mode);
|
|
239
|
+
let pos: SpacingGroupPosition | undefined;
|
|
240
|
+
const nativeFor = makeNativeCache();
|
|
241
|
+
const comp = styledComponentLike((width: number) => {
|
|
242
|
+
const inner = Math.max(3, width - visibleWidth(RAIL));
|
|
243
|
+
const style = options.style ?? renderStyle();
|
|
244
|
+
const lines: string[] = [];
|
|
245
|
+
const first = pos === undefined || pos.index === 0;
|
|
246
|
+
if (first && options.label !== undefined) {
|
|
247
|
+
lines.push(`${dTheme.fg("accent", "◆")} ${dTheme.fg("accent", dTheme.bold(options.label))}`);
|
|
248
|
+
}
|
|
249
|
+
if (step.kind === "text") {
|
|
250
|
+
lines.push(...styledTextLines(style, step.text, { thinking: expanded ? step.thinking : undefined }, dTheme, inner));
|
|
251
|
+
} else {
|
|
252
|
+
const connector: "├" | "└" = pos?.nextKind === "tool" ? "├" : "└";
|
|
253
|
+
const native = style === "regular" ? nativeFor(step, expanded, inner) : undefined;
|
|
254
|
+
if (native !== undefined) {
|
|
255
|
+
lines.push(...native);
|
|
256
|
+
} else {
|
|
257
|
+
lines.push(...styledToolCallLines(style, {
|
|
258
|
+
name: step.name,
|
|
259
|
+
arg: step.arg,
|
|
260
|
+
output: step.output,
|
|
261
|
+
isError: step.isError,
|
|
262
|
+
expanded,
|
|
263
|
+
durationMs: step.durationMs,
|
|
264
|
+
connector: style === "simple" ? connector : undefined,
|
|
265
|
+
}, dTheme, inner));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return lines.map((l) => truncateToWidth(l, inner)).map((l) => paintDelegatedLine(l, width, modeTheme));
|
|
269
|
+
}, () => options.style ?? renderStyle());
|
|
270
|
+
comp.spacingGroup = options.group;
|
|
271
|
+
comp.spacingKind = step.kind === "tool" ? "tool" : "text";
|
|
272
|
+
comp.setGroupPosition = (p: SpacingGroupPosition) => {
|
|
273
|
+
// Only a real position change invalidates — the host re-renders twice per
|
|
274
|
+
// pass (initial + positioned), and identical positions must not churn the
|
|
275
|
+
// native/paint caches (and must never invalidate from inside render).
|
|
276
|
+
const same = pos !== undefined && pos.index === p.index && pos.count === p.count && pos.prevKind === p.prevKind && pos.nextKind === p.nextKind;
|
|
277
|
+
pos = p;
|
|
278
|
+
if (!same) comp.invalidate();
|
|
279
|
+
};
|
|
280
|
+
return comp as unknown as Component & {
|
|
281
|
+
spacingGroup: string;
|
|
282
|
+
spacingKind: string;
|
|
283
|
+
setGroupPosition: (pos: SpacingGroupPosition) => void;
|
|
284
|
+
};
|
|
285
|
+
}
|