elanous 0.2.31 → 0.2.32
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/apps/pwa/out/404/index.html +1 -1
- package/apps/pwa/out/404.html +1 -1
- package/apps/pwa/out/_next/static/chunks/{1834-50625904996a8fa8.js → 1834-98b64172bc1efe77.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/69-0a2c8209c6d4a158.js +1 -0
- package/apps/pwa/out/_next/static/chunks/8097-9530c03bcc550f02.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-504204b58c82c253.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/autopilot/{page-3535a4a69f1fafaa.js → page-a9717a67c2eecbaf.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-5c9ec30aee62dfbf.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-584191533721fa16.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/bots/page-73c4b4bb3ac7ab5f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ceo/{page-5a503541353e60a8.js → page-c0f1ca371f710b06.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/chat/{page-6025101384e361a5.js → page-46430d45a9e04eac.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-3b2ebf1dc64ff0be.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/control/{page-a645f00a06483e9a.js → page-a27b1e5a0d1b308f.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-25654b82094f7d22.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/design-check/{page-e9bab7413b028b28.js → page-8ea49a4c6dd2c6e7.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-ce5113aefea0a8d1.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/exec/{page-e547e6ce55f1fb33.js → page-0e5a41fac1c75396.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/field/{page-d27e1b9850fd54bf.js → page-31aaf1ed37c6df9a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-3dcc1fa411e1f4e0.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/intake/{page-0fec29c7ba01894b.js → page-c69247e7134d329d.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{layout-339a07410cd6f36b.js → layout-66dc5cd3a0999abe.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-26ff7ff651a7217d.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-617b3976981efa96.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/loops/{page-2fd93ed8c9f16580.js → page-d256f5d5d9552823.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/market/{page-9b564dbff0fefc35.js → page-ce8f1907b8a6101c.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-33e182a316263116.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/morning/{page-814ba51385225776.js → page-32866485913d6a04.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/{not-found-397d479626e5a459.js → not-found-7607cca9b84950c6.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/observatory/{page-af1bba4e3251b728.js → page-eeab86176b03dc3b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/checklist/{page-3ad3be5e7d3fdce9.js → page-5b93dbae3432b996.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-77a6cb90a9cf2b10.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/ops/seats/{page-4da93ad35d4a8d28.js → page-f47e768b5b242b5a.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-eb41aa3367fc2505.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/page-4d13ab0b8a267e3f.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-84dad32ff0fc0831.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/scheduler/{page-3a658d5fa6d16a25.js → page-95306f892aa2d0ee.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/sessions/{page-0157de6e85f8bb6b.js → page-940fb6c217f6b470.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/devices/{page-0a07363b130182a1.js → page-8c66a3b24a745630.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/settings/{page-841d73d4ac44ed49.js → page-7b675dfd35c36fc9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/done/{page-6f43a3d1146e6377.js → page-8bd0d3cf7665bcfc.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/setup/{page-521ee23ac8c0abfa.js → page-c219f2ed171131be.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/share/{page-a9992ee285efaa83.js → page-40f8fa21bdae955b.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/showroom/{page-3d48e85fa89ec04f.js → page-468f8313e4f1fbd9.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-7bbb8fa353a246c4.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/term/page-04d00ae9b0890349.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/today/{page-233c71cd5f68f0d7.js → page-4214b855d9233ea8.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/trace/{page-70151d78de2f375e.js → page-936752c7aa58762e.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/vault/{page-dcac33a3e2cb72c0.js → page-ec052ad359df1797.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-8305570551091338.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/workflows/{page-0448145ccd5c45c1.js → page-fad650e4793adb65.js} +1 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-d90e64743826e1d8.js +1 -0
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-a16bcfd49af86a32.js +1 -0
- package/apps/pwa/out/_next/static/css/b5d88eba78a3d7bf.css +1 -0
- package/apps/pwa/out/approvals/index.html +1 -1
- package/apps/pwa/out/approvals/index.txt +14 -14
- package/apps/pwa/out/autopilot/index.html +1 -1
- package/apps/pwa/out/autopilot/index.txt +14 -14
- package/apps/pwa/out/board/index.html +1 -1
- package/apps/pwa/out/board/index.txt +14 -14
- package/apps/pwa/out/botlab/index.html +1 -1
- package/apps/pwa/out/botlab/index.txt +14 -14
- package/apps/pwa/out/bots/index.html +1 -1
- package/apps/pwa/out/bots/index.txt +14 -14
- package/apps/pwa/out/ceo/index.html +1 -1
- package/apps/pwa/out/ceo/index.txt +14 -14
- package/apps/pwa/out/chat/index.html +1 -1
- package/apps/pwa/out/chat/index.txt +14 -14
- package/apps/pwa/out/consult/index.html +1 -1
- package/apps/pwa/out/consult/index.txt +14 -14
- package/apps/pwa/out/control/index.html +1 -1
- package/apps/pwa/out/control/index.txt +14 -14
- package/apps/pwa/out/decisions/index.html +1 -1
- package/apps/pwa/out/decisions/index.txt +14 -14
- package/apps/pwa/out/design-check/index.html +1 -1
- package/apps/pwa/out/design-check/index.txt +14 -14
- package/apps/pwa/out/editor/index.html +1 -1
- package/apps/pwa/out/editor/index.txt +14 -14
- package/apps/pwa/out/exec/index.html +1 -1
- package/apps/pwa/out/exec/index.txt +14 -14
- package/apps/pwa/out/field/index.html +1 -1
- package/apps/pwa/out/field/index.txt +14 -14
- package/apps/pwa/out/healthz +1 -1
- package/apps/pwa/out/index.html +1 -1
- package/apps/pwa/out/index.txt +14 -14
- package/apps/pwa/out/inside/index.html +1 -1
- package/apps/pwa/out/inside/index.txt +14 -14
- package/apps/pwa/out/intake/index.html +1 -1
- package/apps/pwa/out/intake/index.txt +14 -14
- package/apps/pwa/out/live/index.html +1 -1
- package/apps/pwa/out/live/index.txt +17 -14
- package/apps/pwa/out/live-run/index.html +1 -1
- package/apps/pwa/out/live-run/index.txt +14 -14
- package/apps/pwa/out/loops/index.html +1 -1
- package/apps/pwa/out/loops/index.txt +14 -14
- package/apps/pwa/out/market/index.html +1 -1
- package/apps/pwa/out/market/index.txt +14 -14
- package/apps/pwa/out/missions/placeholder/index.html +1 -1
- package/apps/pwa/out/missions/placeholder/index.txt +14 -14
- package/apps/pwa/out/morning/index.html +1 -1
- package/apps/pwa/out/morning/index.txt +14 -14
- package/apps/pwa/out/observatory/index.html +1 -1
- package/apps/pwa/out/observatory/index.txt +14 -14
- package/apps/pwa/out/ops/checklist/index.html +1 -1
- package/apps/pwa/out/ops/checklist/index.txt +14 -14
- package/apps/pwa/out/ops/release/index.html +1 -1
- package/apps/pwa/out/ops/release/index.txt +14 -14
- package/apps/pwa/out/ops/seats/index.html +1 -1
- package/apps/pwa/out/ops/seats/index.txt +14 -14
- package/apps/pwa/out/outputs/index.html +1 -1
- package/apps/pwa/out/outputs/index.txt +14 -14
- package/apps/pwa/out/reflection/index.html +1 -1
- package/apps/pwa/out/reflection/index.txt +15 -15
- package/apps/pwa/out/scheduler/index.html +1 -1
- package/apps/pwa/out/scheduler/index.txt +14 -14
- package/apps/pwa/out/sessions/index.html +1 -1
- package/apps/pwa/out/sessions/index.txt +14 -14
- package/apps/pwa/out/settings/devices/index.html +1 -1
- package/apps/pwa/out/settings/devices/index.txt +14 -14
- package/apps/pwa/out/settings/index.html +1 -1
- package/apps/pwa/out/settings/index.txt +14 -14
- package/apps/pwa/out/setup/done/index.html +1 -1
- package/apps/pwa/out/setup/done/index.txt +14 -14
- package/apps/pwa/out/setup/index.html +1 -1
- package/apps/pwa/out/setup/index.txt +14 -14
- package/apps/pwa/out/share/index.html +1 -1
- package/apps/pwa/out/share/index.txt +14 -14
- package/apps/pwa/out/showroom/index.html +1 -1
- package/apps/pwa/out/showroom/index.txt +14 -14
- package/apps/pwa/out/tasks/index.html +1 -1
- package/apps/pwa/out/tasks/index.txt +14 -14
- package/apps/pwa/out/term/index.html +1 -1
- package/apps/pwa/out/term/index.txt +14 -14
- package/apps/pwa/out/today/index.html +1 -1
- package/apps/pwa/out/today/index.txt +14 -14
- package/apps/pwa/out/trace/index.html +1 -1
- package/apps/pwa/out/trace/index.txt +14 -14
- package/apps/pwa/out/vault/index.html +1 -1
- package/apps/pwa/out/vault/index.txt +14 -14
- package/apps/pwa/out/workflows/chat-ui/index.html +1 -1
- package/apps/pwa/out/workflows/chat-ui/index.txt +14 -14
- package/apps/pwa/out/workflows/index.html +1 -1
- package/apps/pwa/out/workflows/index.txt +14 -14
- package/apps/pwa/out/workspace/index.html +1 -1
- package/apps/pwa/out/workspace/index.txt +14 -14
- package/apps/pwa/out/worktrees/index.html +1 -1
- package/apps/pwa/out/worktrees/index.txt +14 -14
- package/graphs/implement-loop.yaml +5 -0
- package/package.json +1 -1
- package/scripts/ci-typecheck-changed.ts +26 -1
- package/scripts/measure-si-task-cpu.ts +143 -0
- package/scripts/ratchets/test-interference-patterns.allowlist.txt +0 -2
- package/scripts/release-loop/prepare-node.ts +102 -4
- package/src/acp/core-turn-bridge.ts +16 -4
- package/src/acp/elanous-extensions.ts +3 -0
- package/src/acp/server.ts +3 -0
- package/src/acp/tool-trace.ts +186 -11
- package/src/agent/self-ambient-cache.ts +156 -0
- package/src/agent/self-ambient.ts +8 -1
- package/src/agent/task-notification.ts +5 -0
- package/src/agent-substrate/pr-reviewer.ts +3 -1
- package/src/ask-user-question/tool.ts +22 -0
- package/src/autopilot/build/llm-conflict-merge.ts +56 -10
- package/src/autopilot/pr-manager.ts +23 -5
- package/src/capture/frame-state-detect.ts +39 -0
- package/src/capture/tui-self-report.ts +134 -6
- package/src/chat/index.ts +61 -15
- package/src/chat/pickers/modal-runtime.ts +6 -0
- package/src/chat/pickers/modals.ts +8 -2
- package/src/cli/ask-bare.ts +1 -0
- package/src/cli/import-cli.ts +12 -0
- package/src/cli/log-event-names.ts +10 -0
- package/src/cli/msg-cli.ts +1 -0
- package/src/cli/pod-cli.ts +14 -3
- package/src/cli/pr-cli.ts +67 -32
- package/src/cli/pr-land-base-read.ts +34 -0
- package/src/cli/pr-land-heavy-remote.ts +81 -25
- package/src/cli/pr-land-overlap-evidence.ts +46 -2
- package/src/cli/pr-land-pod-shadow-job.ts +26 -3
- package/src/cli/pr-land-refusal.ts +20 -5
- package/src/cli/pty-takeover-cli.ts +88 -14
- package/src/cli/sim-cli.ts +18 -0
- package/src/context-bus/ops-events.ts +6 -2
- package/src/core-turn/run-core-turn.ts +1 -0
- package/src/core-turn/run-goal-loop.ts +21 -0
- package/src/core-turn/types.ts +3 -0
- package/src/dashboard/acp-active-model.ts +42 -1
- package/src/dashboard/background-jobs-status.ts +66 -0
- package/src/dashboard/index.ts +117 -44
- package/src/dashboard/input/agent-wake.ts +242 -0
- package/src/dashboard/input/chat-main-entry-runtime.ts +5 -0
- package/src/dashboard/input/external-input-host.ts +23 -9
- package/src/dashboard/input/mouse-wiring.ts +108 -11
- package/src/dashboard/log-click-runtime.ts +118 -2
- package/src/dashboard/turn-preamble.ts +116 -40
- package/src/dashboard/turn-session-layers.ts +4 -2
- package/src/dashboard/turn-start-segments.ts +222 -0
- package/src/display/prompt-row-owner.ts +146 -0
- package/src/display/resize-listener.ts +33 -2
- package/src/fold-stack.ts +4 -2
- package/src/git-fs/index.ts +263 -36
- package/src/harness/gate-reserve.ts +87 -0
- package/src/harness/harness-incidents.ts +13 -0
- package/src/harness/harness-queue.ts +15 -0
- package/src/index.ts +8 -6
- package/src/input/external-input-queue.ts +34 -9
- package/src/llm/route-decision.ts +36 -0
- package/src/llm/tool-loop-steered-rejection.ts +8 -0
- package/src/llm-request-shape.ts +209 -0
- package/src/llm.ts +166 -267
- package/src/loops/orchestrator/tick.ts +24 -2
- package/src/maturity/feature-maturity.ts +1 -1
- package/src/mss/logging/render-categories.ts +5 -0
- package/src/ops/overview.ts +59 -2
- package/src/pr-tool/census.ts +19 -4
- package/src/pr-tool/inflight-registry.ts +143 -0
- package/src/pty-shell/pty-manifest.ts +12 -3
- package/src/pty-shell/tui-run-state.ts +34 -3
- package/src/seat-loop/traffic-signal.ts +2 -1
- package/src/self-dev/ask-launch-flow.ts +13 -0
- package/src/self-dev/draft-residue.ts +243 -24
- package/src/self-dev/shard-plan.ts +11 -3
- package/src/self-dev/shard-run.ts +103 -10
- package/src/self-implement/elanous-tui-spawn.ts +14 -0
- package/src/self-implement/follow-up-placement.ts +79 -0
- package/src/self-implement/gate-artifacts.ts +15 -4
- package/src/self-implement/gate-baseline.ts +96 -5
- package/src/self-implement/gate-cli.ts +7 -2
- package/src/self-implement/gate-remote.ts +26 -1
- package/src/self-implement/host-regate.ts +36 -4
- package/src/self-implement/orchestrator.ts +101 -11
- package/src/self-implement/run-ledger.ts +16 -14
- package/src/session-runtime/index.ts +22 -267
- package/src/shell-runner/background-surface.ts +15 -5
- package/src/skills/tools/agent.ts +39 -6
- package/src/skills/tools/list-dir.ts +11 -0
- package/src/skills/tools/schedule-wakeup.ts +408 -0
- package/src/skills/tools/shell.ts +245 -3
- package/src/status/bar.ts +26 -0
- package/src/steward/digest-delivery.ts +118 -0
- package/src/steward/hold-hygiene.ts +120 -0
- package/src/steward/triage.ts +10 -0
- package/src/surface/shell-runner-boot.ts +5 -2
- package/src/task-agent/task-hand.ts +24 -0
- package/src/task-orchestrator/surfaces/pod-command-job.ts +65 -11
- package/src/task-orchestrator/surfaces/pod-lease.ts +98 -0
- package/src/task-orchestrator/surfaces/pod-ledger-events.ts +1 -1
- package/src/task-orchestrator/surfaces/pod-pool.ts +3 -1
- package/src/task-orchestrator/surfaces/pod-reuse-cleanup.ts +10 -6
- package/src/task-orchestrator/surfaces/pod-warm-worker-pod.ts +525 -0
- package/src/task-orchestrator/surfaces/pod-warm-worker-runner.ts +90 -0
- package/src/task-orchestrator/surfaces/pod-warm-worker-runtime.ts +156 -2
- package/src/task-orchestrator/surfaces/self-implement-pod.ts +7 -5
- package/src/tui/chat/execution-badge.ts +5 -1
- package/src/tui/input-lag-probe.ts +230 -0
- package/src/tui.ts +4 -0
- package/src/ux-sim/scenario.ts +255 -0
- package/src/version/packed-revision.json +1 -1
- package/apps/pwa/out/_next/static/chunks/8097-92f87bd108322f66.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/approvals/page-bf3634a117322df7.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/board/page-b9f51447f757fbf2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/botlab/page-2ebb8fd59b9461f9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/bots/page-a2fc4e49a4fa4ba8.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/consult/page-f4b630cc8b5273cd.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/decisions/page-61ac22cbab1ee4ec.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/editor/page-c992eafe028b642e.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/inside/page-805ac2e137b7a121.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live/page-349f2752d8cd37ad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/live-run/page-d5c6dcf31c25cecc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/missions/[id]/page-62f4475d3c38fb9b.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/ops/release/page-f219ccdbc0bf27df.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/outputs/page-456666766e5ce6fc.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/page-7404e377e8adb8c5.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/reflection/page-14456240ed2a36ad.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/tasks/page-d2ced71132fac9b2.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/term/page-f08358bdf251adb9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workflows/chat-ui/page-149dc1caba411bb9.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/workspace/page-9cb0c444903da65d.js +0 -1
- package/apps/pwa/out/_next/static/chunks/app/worktrees/page-17e504c396e0e389.js +0 -1
- package/apps/pwa/out/_next/static/css/3f5fc6f96e2cf7b2.css +0 -1
- /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_buildManifest.js +0 -0
- /package/apps/pwa/out/_next/static/{LSXfY0u9QtfDbejY0Ukf_ → IVKr3AlRUVLKsw2fh7eXF}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// ── 배경 자식 완료 알림 `agent-wake` (TUI-AGENT-IDLE-WAKE 조각 1 · 0.2.31 · 2026-10-17) ─────────────────
|
|
2
|
+
//
|
|
3
|
+
// 🚨 무엇을 푸는가: `Agent(..., run_in_background)` 자식(⊕ RunShell bg 완료)이 끝나면
|
|
4
|
+
// `globalTaskNotificationQueue` 에 쌓이고 «다음 턴 시작»에 `<task-notification>` 으로 실린다
|
|
5
|
+
// (turn-preamble drain). 그런데 그 큐의 `onEnqueue` 를 비시험 코드가 아무도 구독하지 않아(구독 0),
|
|
6
|
+
// 쉬는 TUI 는 자식이 끝나도 아무 신호가 없었다 — 10-16 #71 «백그라운드 리뷰 4건 끝나고 12분 방치».
|
|
7
|
+
// ⭐ 이 모듈이 구독 «하나»다(구독 0 → 1). 완료 한 건마다 채팅 한 줄
|
|
8
|
+
// «✓ 위임 끝: <이름> — 다음 턴에 결과»(입력칸 무접촉). 결과 본문은 기존 preamble drain 이 다음 턴에 싣는다.
|
|
9
|
+
// ⛔ 입력칸 버퍼·`injectKey`·`hostControl.submit`·pty 주입 0(대표 비침투 · 10-17 설계 정정).
|
|
10
|
+
// ⭐ 조각 2(TUI-AGENT-IDLE-WAKE-TURN · 0.2.32): `openTurn` 을 넘기면 자리 TUI(`detectSeatTui` — H1·H3 와 같은
|
|
11
|
+
// 판정)에서 H2 문(`startTurnIfIdle`, origin `child-complete`)으로 다음 턴을 연다. 사람 대화 TUI 는 알림만
|
|
12
|
+
// (대표 «다음 턴에 싣기»). 거부 사유는 H2 값 그대로(NotIdle·HumanTyping·Cap …) — 여기서 새 판정을 짓지 않는다.
|
|
13
|
+
// 턴 중(NotIdle)·입력 중(HumanTyping)에 온 완료는 «대기»: 입력칸이 다시 뜰 때(`wrapAgentWakePort`) 그 결과가
|
|
14
|
+
// 아직 큐에 남아 있으면(=어느 턴도 안 실었으면) 한 번만 다시 청한다. 이미 실렸으면(drain) 아무것도 안 한다.
|
|
15
|
+
// ⛔ 같은 알림(taskId)은 한 번만 — 큐 `seen` 이 `clear()` 로 비워져도, 대시보드가 재진입해도 막는다.
|
|
16
|
+
//
|
|
17
|
+
// 세 얼굴:
|
|
18
|
+
// 관측 = debug.log('tui.agent-wake', 'notified' | 'opened' | 'rejected' | 'deferred', { taskId, agent, state, seat, shown, reason })
|
|
19
|
+
// ⊕ H2 자체 관측 `tui.autoturn` opened|rejected { origin:'child-complete' }
|
|
20
|
+
// 인지 = 배경 완료 도착(⊕ 이미 알린 taskId 인가) ⊕ 자리 TUI 인가 ⊕ H2 가 열어 주나
|
|
21
|
+
// 힐링 = 채팅 한 줄로 사람·자리에게 «결과가 왔다»를 보인다 · 자리 TUI 면 H2 로 한 턴(화면 «↻ 자동 재개
|
|
22
|
+
// (child-complete): 위임 끝: <이름>») · 결과는 유실 0(큐 그대로 → 연 턴/다음 턴 preamble) ·
|
|
23
|
+
// 그리기 실패는 shown:false · 거부는 사유 로그 → TF 가 본다(`eln logs --category tui.agent-wake`)
|
|
24
|
+
|
|
25
|
+
import { homedir } from 'node:os';
|
|
26
|
+
import type { TextInputInternalTurnYieldPort } from '../../chat/index.js';
|
|
27
|
+
import { debug } from '../../debug/log.js';
|
|
28
|
+
import type { PendingTaskNotification } from '../../agent/task-notification.js';
|
|
29
|
+
import { detectSeatTui, type SeatDetection } from '../seat-boot-preamble.js';
|
|
30
|
+
import type { InternalTurnRejectReason, StartTurnIfIdleOptions, StartTurnIfIdleResult } from './internal-turn.js';
|
|
31
|
+
|
|
32
|
+
/** 자리 TUI 에서 다음 턴을 여는 길(조각 2). 없으면 알림만(조각 1 동작 그대로). */
|
|
33
|
+
export interface AgentWakeOpenTurnDeps {
|
|
34
|
+
/** H2 문 — 대시보드의 `internalTurnGate.startTurnIfIdle`. */
|
|
35
|
+
startTurnIfIdle: (input: string, opts: StartTurnIfIdleOptions) => StartTurnIfIdleResult;
|
|
36
|
+
getCwd: () => string;
|
|
37
|
+
/** 그 완료가 아직 어느 턴에도 안 실렸나 — 대시보드는 `globalTaskNotificationQueue.has`. «대기» 재시도의 근거. */
|
|
38
|
+
isPending: (taskId: string) => boolean;
|
|
39
|
+
home?: string;
|
|
40
|
+
detectSeat?: (cwd: string) => SeatDetection | null;
|
|
41
|
+
/** 입력칸이 다시 뜬 뒤 재시도를 미루는 자리(마운트 «중»에 물러남을 청하지 않게). 기본 setTimeout 0 · unref. */
|
|
42
|
+
defer?: (fn: () => void) => void;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** 이 모듈이 내는 거부 사유(H2 밖) — 나머지는 H2 `InternalTurnRejectReason` 그대로. */
|
|
46
|
+
export type AgentWakeTurnSkipReason =
|
|
47
|
+
/** 사람 대화 TUI — 알림만, 턴은 안 연다. */
|
|
48
|
+
| 'not-seat'
|
|
49
|
+
/** 자리 판정 자체가 실패했다(권한 오류 등) — 모르면 열지 않는다. */
|
|
50
|
+
| 'seat-unknown';
|
|
51
|
+
|
|
52
|
+
/** H2 가 «지금은 안 된다»로 거부한 것 — 입력칸이 다시 뜰 때 한 번 더 청한다. */
|
|
53
|
+
const DEFERRABLE: ReadonlySet<InternalTurnRejectReason> = new Set(['NotIdle', 'HumanTyping']);
|
|
54
|
+
|
|
55
|
+
export interface AgentWakeDeps {
|
|
56
|
+
/** 큐 구독 — 대시보드는 `globalTaskNotificationQueue.onEnqueue` 를 넘긴다. 해제 함수를 돌려준다. */
|
|
57
|
+
subscribe: (listener: (n: PendingTaskNotification) => void) => () => void;
|
|
58
|
+
/** 채팅 한 줄(입력칸 무접촉). */
|
|
59
|
+
notify: (line: string) => void;
|
|
60
|
+
/** 이미 알린 taskId — 설치를 가로질러 공유한다(기본: 이 컨트롤러만의 집합). */
|
|
61
|
+
handled?: Set<string>;
|
|
62
|
+
log?: (category: string, event: string, data: Record<string, unknown>) => void;
|
|
63
|
+
/** 조각 2 — 자리 TUI 에서 H2 로 다음 턴을 연다. 없으면 알림만. */
|
|
64
|
+
openTurn?: AgentWakeOpenTurnDeps;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface AgentWakeController {
|
|
68
|
+
/** 이 컨트롤러의 구독만 푼다(여러 번 불러도 안전). */
|
|
69
|
+
dispose(): void;
|
|
70
|
+
/** 입력칸이 다시 떴다 — «대기» 중인 완료가 아직 안 실렸으면 H2 로 한 번 더 청한다. */
|
|
71
|
+
onInputMounted(): void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function displayName(n: Pick<PendingTaskNotification, 'label' | 'agentName'>): string {
|
|
75
|
+
return ((n.label ?? '').trim() || n.agentName).replace(/\s+/g, ' ').slice(0, 60);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 채팅 한 줄 — «✓ 위임 끝: <이름> — 다음 턴에 결과»(실패·중단이면 ✗ ⊕ 상태). */
|
|
79
|
+
export function formatAgentWakeNotice(n: Pick<PendingTaskNotification, 'label' | 'agentName' | 'state'>): string {
|
|
80
|
+
const name = displayName(n);
|
|
81
|
+
const glyph = n.state === 'done' ? '✓' : '✗';
|
|
82
|
+
const state = n.state === 'done' ? '' : ` (${n.state})`;
|
|
83
|
+
return `${glyph} 위임 끝: ${name}${state} — 다음 턴에 결과`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** 자리 TUI 에서 여는 내부 턴의 입력 — 결과 본문은 turn-preamble 이 `<task-notification>` 으로 싣는다. */
|
|
87
|
+
export function formatAgentWakePrompt(n: Pick<PendingTaskNotification, 'label' | 'agentName' | 'state'>): string {
|
|
88
|
+
return `[자동 재개] 위임 끝: ${displayName(n)} (${n.state}) — <task-notification> 의 결과를 읽고 이어서 처리`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createAgentWake(deps: AgentWakeDeps): AgentWakeController {
|
|
92
|
+
const log = deps.log ?? ((c, e, d) => debug.log(c, e, d));
|
|
93
|
+
const handled = deps.handled ?? new Set<string>();
|
|
94
|
+
|
|
95
|
+
const open = deps.openTurn;
|
|
96
|
+
const home = open?.home ?? homedir();
|
|
97
|
+
const detect = open?.detectSeat ?? ((cwd: string) => detectSeatTui(cwd, { home }));
|
|
98
|
+
const defer = open?.defer ?? ((fn: () => void) => {
|
|
99
|
+
const t = setTimeout(fn, 0);
|
|
100
|
+
(t as { unref?: () => void }).unref?.();
|
|
101
|
+
});
|
|
102
|
+
/** H2 가 «지금은 안 된다»로 거부한 마지막 완료 — 입력칸이 다시 뜰 때 한 번만 다시 청한다. */
|
|
103
|
+
let deferred: PendingTaskNotification | null = null;
|
|
104
|
+
|
|
105
|
+
const tryOpen = (n: PendingTaskNotification, trigger: 'enqueue' | 'input-mounted'): void => {
|
|
106
|
+
if (!open) return;
|
|
107
|
+
const base = { taskId: n.taskId, agent: n.agentName, state: n.state, trigger };
|
|
108
|
+
let seat: string | null = null;
|
|
109
|
+
try { seat = detect(open.getCwd())?.seat ?? null; } catch (error) {
|
|
110
|
+
log('tui.agent-wake', 'rejected', { ...base, reason: 'seat-unknown' satisfies AgentWakeTurnSkipReason,
|
|
111
|
+
detail: error instanceof Error ? error.message : String(error) });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!seat) {
|
|
115
|
+
log('tui.agent-wake', 'rejected', { ...base, seat, reason: 'not-seat' satisfies AgentWakeTurnSkipReason });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
let result: StartTurnIfIdleResult;
|
|
119
|
+
try {
|
|
120
|
+
result = open.startTurnIfIdle(formatAgentWakePrompt(n), { origin: 'child-complete', reason: `위임 끝: ${displayName(n)}` });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
log('tui.agent-wake', 'rejected', { ...base, seat, reason: 'error', detail: error instanceof Error ? error.message : String(error) });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (result.opened) {
|
|
126
|
+
deferred = null;
|
|
127
|
+
log('tui.agent-wake', 'opened', { ...base, seat, turnId: result.turnId });
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
const wait = DEFERRABLE.has(result.reason) && trigger === 'enqueue';
|
|
131
|
+
if (wait) deferred = n;
|
|
132
|
+
log('tui.agent-wake', wait ? 'deferred' : 'rejected', { ...base, seat, reason: result.reason, ...(result.detail ? { detail: result.detail } : {}) });
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const handle = (n: PendingTaskNotification): boolean => {
|
|
136
|
+
const base = { taskId: n.taskId, agent: n.agentName, state: n.state };
|
|
137
|
+
if (handled.has(n.taskId)) {
|
|
138
|
+
log('tui.agent-wake', 'rejected', { ...base, reason: 'duplicate' });
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
handled.add(n.taskId);
|
|
142
|
+
// 그리기 실패가 큐·형제 리스너를 막지 않는다 — 대신 관측에 남긴다(알림이 실제로 안 보였음을 볼 수 있게).
|
|
143
|
+
let notifyError: string | null = null;
|
|
144
|
+
try { deps.notify(formatAgentWakeNotice(n)); } catch (error) {
|
|
145
|
+
notifyError = error instanceof Error ? error.message : String(error);
|
|
146
|
+
}
|
|
147
|
+
log('tui.agent-wake', 'notified', { ...base, shown: notifyError === null, ...(notifyError !== null ? { notifyError } : {}) });
|
|
148
|
+
if (open) tryOpen(n, 'enqueue');
|
|
149
|
+
return true;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
let unsubscribe: (() => void) | null = deps.subscribe((n) => { handle(n); });
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
dispose() {
|
|
156
|
+
const off = unsubscribe;
|
|
157
|
+
unsubscribe = null;
|
|
158
|
+
deferred = null;
|
|
159
|
+
off?.();
|
|
160
|
+
},
|
|
161
|
+
onInputMounted() {
|
|
162
|
+
if (!open || !deferred || unsubscribe === null) return;
|
|
163
|
+
const n = deferred;
|
|
164
|
+
deferred = null; // 재시도는 «한 번» — 다시 거부돼도 쌓지 않는다(결과는 다음 턴 preamble 이 싣는다).
|
|
165
|
+
defer(() => {
|
|
166
|
+
try {
|
|
167
|
+
// 예약 뒤 이 대시보드가 닫혔다 → 옛 H2 문에 청하지 않는다.
|
|
168
|
+
if (unsubscribe === null) return;
|
|
169
|
+
// 그 사이 어느 턴이 «그» 결과를 이미 실었다(drain) → 열 이유가 없다(다른 결과는 자기 enqueue 가 청한다).
|
|
170
|
+
if (!open.isPending(n.taskId)) {
|
|
171
|
+
log('tui.agent-wake', 'rejected', { taskId: n.taskId, agent: n.agentName, state: n.state, trigger: 'input-mounted', reason: 'already-carried' });
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
tryOpen(n, 'input-mounted');
|
|
175
|
+
} catch { /* fail-soft: 깨움은 입력을 막지 않는다 */ }
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** 대시보드 `notify` — 채팅 줄을 넣고 그린다. ⛔ 예외를 삼키지 않는다: 줄 넣기·그리기가 실패하면 던져서
|
|
182
|
+
* `handle` 이 `notified{shown:false, notifyError}` 로 남기게 한다(삼키면 «보였다»고 거짓 관측). */
|
|
183
|
+
export function createDashboardAgentWakeNotify(
|
|
184
|
+
pushLine: (line: string) => void,
|
|
185
|
+
draw: () => void,
|
|
186
|
+
): (line: string) => void {
|
|
187
|
+
return (line) => {
|
|
188
|
+
pushLine(line);
|
|
189
|
+
draw();
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── 대시보드 수명 ────────────────────────────────────────────────────────────────────
|
|
194
|
+
// showDashboard 는 다시 들어올 수 있다. ① 구독은 언제나 하나 — 설치가 앞 설치를 먼저 푼다.
|
|
195
|
+
// ② 각 대시보드는 «자기» 컨트롤러만 푼다(늦게 닫힌 앞 대시보드가 새 구독을 끊지 않는다).
|
|
196
|
+
// ③ 이미 알린 taskId 는 설치를 가로질러 하나의 집합 — 재진입 뒤 큐 clear ⊕ 같은 taskId 도 다시 안 알린다
|
|
197
|
+
// (큐 `seen` 과 같은 수명·같은 크기 정책: 한 세션 O(100) 건).
|
|
198
|
+
const sharedHandled = new Set<string>();
|
|
199
|
+
let installed: AgentWakeController | null = null;
|
|
200
|
+
// ④ 프로세스 종료 정리는 모듈에 «하나»만 — 재진입마다 beforeExit 리스너가 쌓이지 않게(설치 수와 무관하게 1개).
|
|
201
|
+
let exitHookArmed = false;
|
|
202
|
+
export function agentWakeBeforeExitHook(): void {
|
|
203
|
+
installed?.dispose();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** 입력칸 문(H2 port)을 감싼다 — 입력칸이 뜰 때마다 «지금 설치된» 깨움에 알린다(port 는 설치보다 먼저 만들어진다). */
|
|
207
|
+
export function wrapAgentWakePort(port: TextInputInternalTurnYieldPort): TextInputInternalTurnYieldPort {
|
|
208
|
+
return {
|
|
209
|
+
attach(requestYield) {
|
|
210
|
+
port.attach(requestYield);
|
|
211
|
+
try { installed?.onInputMounted(); } catch { /* fail-soft */ }
|
|
212
|
+
},
|
|
213
|
+
detach(opts) { port.detach(opts); },
|
|
214
|
+
declined(detail) { port.declined?.(detail); },
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** 대시보드용 설치 — 앞 설치를 먼저 풀고(구독 정확히 1) 공유 dedupe 집합을 쓴다. */
|
|
219
|
+
export function installAgentWake(deps: Omit<AgentWakeDeps, 'handled'>): AgentWakeController {
|
|
220
|
+
installed?.dispose();
|
|
221
|
+
if (!exitHookArmed) {
|
|
222
|
+
exitHookArmed = true;
|
|
223
|
+
process.on('beforeExit', agentWakeBeforeExitHook);
|
|
224
|
+
}
|
|
225
|
+
const ctl = createAgentWake({ ...deps, handled: sharedHandled });
|
|
226
|
+
const own: AgentWakeController = {
|
|
227
|
+
dispose() {
|
|
228
|
+
ctl.dispose();
|
|
229
|
+
if (installed === own) installed = null;
|
|
230
|
+
},
|
|
231
|
+
onInputMounted() { ctl.onInputMounted(); },
|
|
232
|
+
};
|
|
233
|
+
installed = own;
|
|
234
|
+
return own;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** 시험 격리용 — 설치·공유 dedupe 를 비운다. */
|
|
238
|
+
export function resetAgentWakeForTest(): void {
|
|
239
|
+
installed?.dispose();
|
|
240
|
+
installed = null;
|
|
241
|
+
sharedHandled.clear();
|
|
242
|
+
}
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
TextInputHost,
|
|
6
6
|
} from '../../chat/index.js';
|
|
7
7
|
import type { PromptFrame } from '../../display/prompt-frame.js';
|
|
8
|
+
import type { PromptRowOwner } from '../../display/prompt-row-owner.js';
|
|
8
9
|
import type { SurfaceRegistry } from '../../surface/index.js';
|
|
9
10
|
import { createChatMainInputHost } from './chat-main-host.js';
|
|
10
11
|
import { createChatMainInputSession } from './chat-main-session.js';
|
|
@@ -24,6 +25,9 @@ export interface RunDashboardChatMainEntryDeps {
|
|
|
24
25
|
insertAtCursor?: (text: string) => void;
|
|
25
26
|
submit?: (request?: string | TextInputExternalSubmitRequest) => void;
|
|
26
27
|
};
|
|
28
|
+
/** TUI-PROMPT-SINGLE-OWNER — the frame renderer draws the prompt band
|
|
29
|
+
* from what textInput publishes here; textInput writes no prompt rows. */
|
|
30
|
+
promptRowOwner?: PromptRowOwner;
|
|
27
31
|
setInputLines: (visibleLines: number) => boolean;
|
|
28
32
|
redraw: () => void;
|
|
29
33
|
dispatchGlobalAction: (action: TextInputGlobalAction) => void | Promise<void>;
|
|
@@ -84,6 +88,7 @@ export async function runDashboardChatMainEntry(
|
|
|
84
88
|
textInputOpts: {
|
|
85
89
|
...deps.textInputOpts,
|
|
86
90
|
host,
|
|
91
|
+
...(deps.promptRowOwner ? { promptRowOwner: deps.promptRowOwner } : {}),
|
|
87
92
|
},
|
|
88
93
|
});
|
|
89
94
|
// textInput has returned: its paint closure now holds the submitted buffer.
|
|
@@ -16,12 +16,33 @@ export interface DashboardExternalInputHostDeps {
|
|
|
16
16
|
};
|
|
17
17
|
/** 채팅 로그에 한 줄(표지)을 남기고 다시 그린다. */
|
|
18
18
|
pushNotice: (line: string) => void;
|
|
19
|
+
/** 외부 입력 대기열의 현재 상태 한 줄을 설정하거나(문면) 지운다(null). */
|
|
20
|
+
setQueueNotice: (line: string | null) => void;
|
|
19
21
|
/** 지금 턴의 ESC 중단 경로(escGate) — 턴 밖이면 null. 외부 `/stop` 이 이것을 부른다. */
|
|
20
22
|
getTurnStop?: () => (() => void) | null;
|
|
21
23
|
/** 미소비 키 수 — 기본 tui.ts 키 큐. */
|
|
22
24
|
pendingKeyCount?: () => number;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
export function updateDashboardExternalInputQueueNotice(
|
|
28
|
+
lines: string[],
|
|
29
|
+
previousNotice: string | null,
|
|
30
|
+
nextNotice: string | null,
|
|
31
|
+
formatNotice: (notice: string) => string = (notice) => notice,
|
|
32
|
+
): boolean {
|
|
33
|
+
const previousLine = previousNotice === null ? null : formatNotice(previousNotice);
|
|
34
|
+
const nextLine = nextNotice === null ? null : formatNotice(nextNotice);
|
|
35
|
+
if (previousLine === nextLine) return false;
|
|
36
|
+
const previousIndex = previousLine === null ? -1 : lines.indexOf(previousLine);
|
|
37
|
+
if (previousIndex >= 0) {
|
|
38
|
+
if (nextLine === null) lines.splice(previousIndex, 1);
|
|
39
|
+
else lines[previousIndex] = nextLine;
|
|
40
|
+
} else if (nextLine !== null) {
|
|
41
|
+
lines.push(nextLine);
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
25
46
|
export function createDashboardExternalInputHost(deps: DashboardExternalInputHostDeps): ExternalInputHost {
|
|
26
47
|
// ⚠️ submit/insertAtCursor 는 textInput 이 내려간 뒤에도 남아 있다 — «composer 가 떠 있나»는
|
|
27
48
|
// getBufferText 유무로만 판단한다(내려가면 undefined).
|
|
@@ -45,15 +66,8 @@ export function createDashboardExternalInputHost(deps: DashboardExternalInputHos
|
|
|
45
66
|
: result === 'no-turn' ? '⏹ 외부 /stop — 도는 턴이 없어 무시했습니다'
|
|
46
67
|
: '⏹ 외부 /stop — 이 턴의 중단 경로에 닿지 못했습니다(Esc 로 멈춰 주세요)');
|
|
47
68
|
},
|
|
48
|
-
onQueueChange: (pending
|
|
49
|
-
|
|
50
|
-
const notice = renderExternalInputQueueNotice(pending);
|
|
51
|
-
if (notice) deps.pushNotice(notice);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
deps.pushNotice(pending.length > 0
|
|
55
|
-
? `⤵ 외부 입력 1건 전송 · 남은 대기 ${pending.length}건`
|
|
56
|
-
: '⤵ 외부 입력 1건 전송');
|
|
69
|
+
onQueueChange: (pending) => {
|
|
70
|
+
deps.setQueueNotice(renderExternalInputQueueNotice(pending));
|
|
57
71
|
},
|
|
58
72
|
};
|
|
59
73
|
}
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// cross-cutting concern. Unit-testable in isolation.
|
|
22
22
|
|
|
23
23
|
import chalk from 'chalk';
|
|
24
|
-
import { ctp, stripAnsi, visibleWidth } from '../../tui.js';
|
|
24
|
+
import { ctp, stripAnsi, truncate, visibleWidth } from '../../tui.js';
|
|
25
25
|
import { debug } from '../../debug/log.js';
|
|
26
26
|
import {
|
|
27
27
|
modalMoveThreshold,
|
|
@@ -77,6 +77,7 @@ import {
|
|
|
77
77
|
modeSegment,
|
|
78
78
|
ptyShellCountSegment,
|
|
79
79
|
runningAgentsSegment,
|
|
80
|
+
backgroundJobsSegment,
|
|
80
81
|
runningBuildSegment,
|
|
81
82
|
controllerSegment,
|
|
82
83
|
shellRollupSegment,
|
|
@@ -88,6 +89,7 @@ import {
|
|
|
88
89
|
type GitSegmentState,
|
|
89
90
|
type RunningBuildState,
|
|
90
91
|
type ShellRollupSummary,
|
|
92
|
+
type BackgroundJobsSummary,
|
|
91
93
|
type StatusBarRenderOptions,
|
|
92
94
|
type VwSummary,
|
|
93
95
|
} from '../../status/bar.js';
|
|
@@ -169,6 +171,9 @@ export interface DashboardStatusInput {
|
|
|
169
171
|
elapsedSec?: number;
|
|
170
172
|
/** Running registered child agents. Omit or pass zero to hide. */
|
|
171
173
|
runningAgents?: number;
|
|
174
|
+
/** TUI-BG-JOB-VISIBILITY (#71) — turn-outliving shell jobs. Rendered as
|
|
175
|
+
* `⧗ 백그라운드 N` after the agents/controller pills. Omit or 0 to hide. */
|
|
176
|
+
backgroundJobs?: BackgroundJobsSummary | null;
|
|
172
177
|
/** External controller identity. Omit to hide. */
|
|
173
178
|
controller?: string;
|
|
174
179
|
shellCount?: number;
|
|
@@ -742,6 +747,62 @@ function createPopupAuthority(deps: {
|
|
|
742
747
|
};
|
|
743
748
|
}
|
|
744
749
|
|
|
750
|
+
/** 상태줄 세그먼트를 폭 `budget` 안에 순서대로 놓는다 — 넘치는 세그먼트는 건너뛰고(뒤의 작은 것은
|
|
751
|
+
* 들어가면 넣는다) `required` 세그먼트(첫 칸 ⊕ 호출자가 지정한 필수 표시)는 늘 둔다: 선택 세그먼트는
|
|
752
|
+
* «뒤에 남은 필수 세그먼트 몫» 을 남겨 둘 수 있을 때만 들어간다. 필수만으로 넘치면 `width > budget`
|
|
753
|
+
* 으로 드러나고 호출자가 잘라 낸다. tf-bg-ghost(10-17): 폭을 넘긴 마지막 줄이 화면을 스크롤해 유령 줄을 남겼다. */
|
|
754
|
+
export function fitStatusSegments(
|
|
755
|
+
widths: readonly number[],
|
|
756
|
+
sepWidth: number,
|
|
757
|
+
budget: number,
|
|
758
|
+
required: readonly boolean[] = [],
|
|
759
|
+
): { placed: Array<{ index: number; startCol: number; width: number }>; dropped: number[]; width: number } {
|
|
760
|
+
const isRequired = (i: number): boolean => i === 0 || required[i] === true;
|
|
761
|
+
const w = (i: number): number => Math.max(0, widths[i] ?? 0);
|
|
762
|
+
// reserve[i] = i 뒤(제외)에 남은 필수 세그먼트가 차지할 폭(구분자 포함).
|
|
763
|
+
const reserve: number[] = new Array(widths.length + 1).fill(0);
|
|
764
|
+
for (let i = widths.length - 1; i >= 0; i--) {
|
|
765
|
+
reserve[i] = reserve[i + 1]! + (isRequired(i + 1) && i + 1 < widths.length ? sepWidth + w(i + 1) : 0);
|
|
766
|
+
}
|
|
767
|
+
const placed: Array<{ index: number; startCol: number; width: number }> = [];
|
|
768
|
+
const dropped: number[] = [];
|
|
769
|
+
let col = 0;
|
|
770
|
+
for (let i = 0; i < widths.length; i++) {
|
|
771
|
+
const start = placed.length > 0 ? col + sepWidth : 0;
|
|
772
|
+
if (!isRequired(i) && start + w(i) + reserve[i]! > budget) {
|
|
773
|
+
dropped.push(i);
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
placed.push({ index: i, startCol: start, width: w(i) });
|
|
777
|
+
col = start + w(i);
|
|
778
|
+
}
|
|
779
|
+
return { placed, dropped, width: col };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** 세그먼트 «종류» — 렌더 문자열의 첫 낱말(🐚 · ctx · 🖥 · ⧗ …). 관측 키에 값이 섞이지 않게. */
|
|
783
|
+
export function statusSegmentKind(render: string): string {
|
|
784
|
+
return stripAnsi(render).trim().split(/\s+/)[0] ?? '';
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/** `line` 을 실폭(visibleWidth) `budget` 이하가 될 때까지 자른다 — 상한 없는 축소 · 0 이면 빈 문자열.
|
|
788
|
+
* `truncate` 는 코드포인트 폭(isWide)으로 세서 이모지에서 어긋날 수 있어 실폭으로 다시 잰다. */
|
|
789
|
+
export function truncateToVisibleWidth(line: string, budget: number): string {
|
|
790
|
+
if (budget <= 0) return '';
|
|
791
|
+
if (visibleWidth(line) <= budget) return line;
|
|
792
|
+
for (let max = budget; max > 0; max--) {
|
|
793
|
+
const cut = truncate(line, max);
|
|
794
|
+
if (visibleWidth(cut) <= budget) return cut;
|
|
795
|
+
}
|
|
796
|
+
return '';
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** 잘린 상태줄에 맞춰 pill 경계를 보이는 폭 안으로 줄인다(화면 밖 pill 은 뺀다). */
|
|
800
|
+
export function clampPillsToWidth(pills: readonly PillBound[], shown: number): PillBound[] {
|
|
801
|
+
return pills
|
|
802
|
+
.filter((p) => p.startCol < shown)
|
|
803
|
+
.map((p) => ({ ...p, endCol: Math.min(p.endCol, shown) }));
|
|
804
|
+
}
|
|
805
|
+
|
|
745
806
|
export function createDashboardMouseWiring(deps: DashboardMouseWiringDeps): DashboardMouseWiring {
|
|
746
807
|
const toastStack = new ToastStack({
|
|
747
808
|
placement: 'top-right',
|
|
@@ -871,6 +932,7 @@ export function createDashboardMouseWiring(deps: DashboardMouseWiringDeps): Dash
|
|
|
871
932
|
}
|
|
872
933
|
}
|
|
873
934
|
|
|
935
|
+
let lastStatusOverflowKey = '';
|
|
874
936
|
const buildStatusLine = (input: DashboardStatusInput): string => {
|
|
875
937
|
// 2026-05-05 — narrow-width gate. modelSegment honours
|
|
876
938
|
// `compact: true` to truncate model id at the first parameter-size
|
|
@@ -879,7 +941,7 @@ export function createDashboardMouseWiring(deps: DashboardMouseWiringDeps): Dash
|
|
|
879
941
|
// terminal" boundary; everything above stays full-width.
|
|
880
942
|
const { cols } = deps.termSize();
|
|
881
943
|
const opts: StatusBarRenderOptions = { theme: input.theme, compact: cols < 100 };
|
|
882
|
-
const segments: Array<{ name: PillName | null; render: string }> = [
|
|
944
|
+
const segments: Array<{ name: PillName | null; render: string; required?: boolean }> = [
|
|
883
945
|
{ name: 'workingDir', render: sessionCwdSegment(input.swd, opts) },
|
|
884
946
|
{ name: null, render: gitSegment(input.swd, labelGitState(input.gitState, input.runningBuild ? '트리' : undefined), opts) },
|
|
885
947
|
];
|
|
@@ -890,6 +952,11 @@ export function createDashboardMouseWiring(deps: DashboardMouseWiringDeps): Dash
|
|
|
890
952
|
if (runningAgentsRender) segments.push({ name: null, render: runningAgentsRender });
|
|
891
953
|
const controllerRender = controllerSegment(input.controller, opts);
|
|
892
954
|
if (controllerRender) segments.push({ name: null, render: controllerRender });
|
|
955
|
+
// TUI-BG-JOB-VISIBILITY (#71) — turn-outliving work sits with the agents/controller
|
|
956
|
+
// «activity» cluster (controller stays immediately after agents).
|
|
957
|
+
const backgroundJobsRender = backgroundJobsSegment(input.backgroundJobs ?? null, opts);
|
|
958
|
+
// 폭이 모자라도 이 표시는 건너뛰지 않는다(필수) — 다른 세그먼트를 먼저 건너뛴다.
|
|
959
|
+
if (backgroundJobsRender) segments.push({ name: null, render: backgroundJobsRender, required: true });
|
|
893
960
|
// A1 — mode pill between model and elapsed. `'general'` yields
|
|
894
961
|
// empty render so the pill disappears by default; only sync /
|
|
895
962
|
// control get painted. Name 'mode' is always set so click still
|
|
@@ -945,20 +1012,50 @@ export function createDashboardMouseWiring(deps: DashboardMouseWiringDeps): Dash
|
|
|
945
1012
|
// Compute column positions as segments concatenate with the same
|
|
946
1013
|
// separator string the renderer uses. `visibleWidth` counts cells
|
|
947
1014
|
// (wide-char aware), so the pill bounds stay correct.
|
|
1015
|
+
//
|
|
1016
|
+
// TUI-BG-JOB-VISIBILITY 라이브 결함(tf-bg-ghost · 10-17) — 상태줄은 «마지막 줄» 이라 폭을 넘으면
|
|
1017
|
+
// 터미널 자동 줄바꿈이 화면 전체를 한 줄 위로 «스크롤» 한다. 차분 렌더러(`render`)는 그걸 모르고
|
|
1018
|
+
// 안 바뀐 줄을 다시 안 써서, 스피너 줄이 ChatLog 아래 유령으로 남고 머리 줄이 사라졌다.
|
|
1019
|
+
// ⇒ 프레임 폭(cols-1 · composeVertical 의 totalW)을 넘는 세그먼트는 «건너뛴다»(순서 유지 ·
|
|
1020
|
+
// 뒤의 작은 세그먼트는 들어갈 수 있으면 넣는다). 관측 = status.bar/overflow-fit(건너뛴 집합이 바뀔 때만).
|
|
948
1021
|
const nextPills: PillBound[] = [];
|
|
949
1022
|
const parts: string[] = [];
|
|
950
|
-
|
|
951
|
-
const
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
1023
|
+
const budget = Math.max(0, cols - 1);
|
|
1024
|
+
const fit = fitStatusSegments(
|
|
1025
|
+
segments.map((seg) => visibleWidth(stripAnsi(seg.render))),
|
|
1026
|
+
visibleWidth(stripAnsi(STATUS_SEGMENT_SEPARATOR)),
|
|
1027
|
+
budget,
|
|
1028
|
+
segments.map((seg) => seg.required === true),
|
|
1029
|
+
);
|
|
1030
|
+
for (const placed of fit.placed) {
|
|
1031
|
+
const seg = segments[placed.index]!;
|
|
1032
|
+
if (seg.name) nextPills.push({ name: seg.name, startCol: placed.startCol, endCol: placed.startCol + placed.width });
|
|
958
1033
|
parts.push(seg.render);
|
|
959
1034
|
}
|
|
1035
|
+
// 키 = 건너뛴 세그먼트 «종류»(첫 낱말 · 🐚·ctx·🖥 …)의 중복 없는 정렬 집합 — 폭·리사이즈·값(ctx 15k→16k)·같은 종류 수는 키가 아니다.
|
|
1036
|
+
const droppedKinds = [...new Set(fit.dropped.map((i) => statusSegmentKind(segments[i]!.render)))].sort();
|
|
1037
|
+
const droppedKey = droppedKinds.join(' ');
|
|
1038
|
+
if (droppedKey !== lastStatusOverflowKey) {
|
|
1039
|
+
lastStatusOverflowKey = droppedKey;
|
|
1040
|
+
try {
|
|
1041
|
+
debug.log('status.bar', 'overflow-fit', {
|
|
1042
|
+
cols,
|
|
1043
|
+
budget,
|
|
1044
|
+
dropped: droppedKinds,
|
|
1045
|
+
width: fit.width,
|
|
1046
|
+
});
|
|
1047
|
+
} catch { /* 관측 실패가 상태줄을 막지 않는다 */ }
|
|
1048
|
+
}
|
|
960
1049
|
pills = nextPills;
|
|
961
|
-
|
|
1050
|
+
const line = parts.join(STATUS_SEGMENT_SEPARATOR);
|
|
1051
|
+
// 첫 칸 하나만으로도 넘치는 아주 좁은 화면 — 잘라서라도 한 줄 안에 둔다.
|
|
1052
|
+
if (fit.width <= budget) return line;
|
|
1053
|
+
// `truncate` 는 코드포인트 폭(isWide)으로 세서 이모지에서 한두 칸 어긋날 수 있다 — 실폭(visibleWidth)으로 다시 잰다.
|
|
1054
|
+
const cut = truncateToVisibleWidth(line, budget);
|
|
1055
|
+
// pill 경계도 실제로 보이는 범위로 줄인다(잘린 뒤의 클릭·호버가 화면 밖을 가리키지 않게).
|
|
1056
|
+
const shown = visibleWidth(cut);
|
|
1057
|
+
pills = clampPillsToWidth(nextPills, shown);
|
|
1058
|
+
return cut;
|
|
962
1059
|
};
|
|
963
1060
|
|
|
964
1061
|
const setStatusRow = (row: number | null): void => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { Attachment, ContextRegistry } from '../context.js';
|
|
2
2
|
import { debug } from '../debug/log.js';
|
|
3
|
-
import type { FoldStack } from '../fold-stack.js';
|
|
3
|
+
import type { FoldStack, FoldTarget } from '../fold-stack.js';
|
|
4
4
|
import type { AttachmentRowMap } from '../log-pane/attachment-row-map.js';
|
|
5
5
|
import type { LogClickDispatchDeps } from '../log-pane/click-dispatch.js';
|
|
6
6
|
import type { AttachmentPopupAction } from '../log-pane/attachment-popup.js';
|
|
@@ -43,6 +43,113 @@ export interface DashboardLogClickRuntimeDeps {
|
|
|
43
43
|
* `toggleAtLine` decides). Hosts
|
|
44
44
|
* that omit it keep the legacy attachment/pill-only behavior. */
|
|
45
45
|
foldStack?: FoldStack;
|
|
46
|
+
/** Keeps the viewport where the click happened while a click toggles a
|
|
47
|
+
* fold (#81). Hosts that omit it keep FoldStack's own scroll policy. */
|
|
48
|
+
foldClickAnchor?: FoldClickScrollAnchor;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Scroll state the fold-click anchor reads and writes. Offsets are the
|
|
52
|
+
* log pane's visual-row offsets (`-1` = pinned to the tail). */
|
|
53
|
+
export interface FoldClickScrollState {
|
|
54
|
+
getScrollOffset: () => number;
|
|
55
|
+
setScrollOffset: (offset: number) => void;
|
|
56
|
+
/** Max visual-row offset for the current chatLines (after a splice). */
|
|
57
|
+
maxScroll: () => number;
|
|
58
|
+
getFrozenTail: () => number | null;
|
|
59
|
+
setFrozenTail: (index: number | null) => void;
|
|
60
|
+
/** chatLines length — tells new output from the fold's own splice. */
|
|
61
|
+
lineCount: () => number;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface FoldClickScrollAnchor {
|
|
65
|
+
/** Current top visual row and raw offset — the click's frame. */
|
|
66
|
+
viewport(): { top: number; scrollOffset: number };
|
|
67
|
+
/** Run a click toggle with the viewport top held in place. */
|
|
68
|
+
run<T>(toggle: () => T): T;
|
|
69
|
+
/** FoldStack `onAfterToggle` hook: inside `run` it keeps the top row;
|
|
70
|
+
* outside (the `f` key, bulk toggles) it falls back to `otherwise`. */
|
|
71
|
+
onAfterToggle(target: FoldTarget, delta: number): void;
|
|
72
|
+
/** Call before each paint: a pane that was following the tail when the
|
|
73
|
+
* click expanded a block goes back to the tail as soon as new output
|
|
74
|
+
* arrives (the click must not leave the pane stuck off the tail). */
|
|
75
|
+
reconcile(): void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** #81 — the old `onAfterToggle` pinned every toggle to the tail, so a
|
|
79
|
+
* click that expanded a block near the bottom pushed its header up the
|
|
80
|
+
* screen and a second click on the same spot landed in the body (rejected
|
|
81
|
+
* as `expanded-body`). A click toggle now keeps the top row; it only
|
|
82
|
+
* returns to the tail when the kept top would be past the new bottom. */
|
|
83
|
+
export function createFoldClickScrollAnchor(
|
|
84
|
+
state: FoldClickScrollState,
|
|
85
|
+
otherwise: (target: FoldTarget, delta: number) => void,
|
|
86
|
+
): FoldClickScrollAnchor {
|
|
87
|
+
let heldTop: number | null = null;
|
|
88
|
+
// Set when a click moved a tail-following pane off the tail: the offset
|
|
89
|
+
// it set and the line count right after the splice.
|
|
90
|
+
let followTail: { offset: number; lineCount: number } | null = null;
|
|
91
|
+
const viewport = (): { top: number; scrollOffset: number } => {
|
|
92
|
+
const scrollOffset = state.getScrollOffset();
|
|
93
|
+
const max = state.maxScroll();
|
|
94
|
+
return { top: scrollOffset < 0 ? max : Math.min(scrollOffset, max), scrollOffset };
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
viewport,
|
|
98
|
+
run<T>(toggle: () => T): T {
|
|
99
|
+
const prev = heldTop;
|
|
100
|
+
heldTop = viewport().top;
|
|
101
|
+
try {
|
|
102
|
+
return toggle();
|
|
103
|
+
} finally {
|
|
104
|
+
heldTop = prev;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
onAfterToggle(target, delta) {
|
|
108
|
+
if (heldTop === null) {
|
|
109
|
+
otherwise(target, delta);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// The splice moved every line after the block; a frozen tail past it
|
|
113
|
+
// must move with them or the pane would cut the expanded rows short.
|
|
114
|
+
const frozen = state.getFrozenTail();
|
|
115
|
+
if (frozen !== null && target.kind === 'static' && target.lineStart < frozen) {
|
|
116
|
+
state.setFrozenTail(frozen + delta);
|
|
117
|
+
}
|
|
118
|
+
const offsetBefore = state.getScrollOffset();
|
|
119
|
+
const wasFollowing = offsetBefore < 0 || followTail?.offset === offsetBefore;
|
|
120
|
+
const max = state.maxScroll();
|
|
121
|
+
const next = heldTop >= max ? -1 : heldTop;
|
|
122
|
+
state.setScrollOffset(next);
|
|
123
|
+
if (next < 0) state.setFrozenTail(null);
|
|
124
|
+
// No freeze here: the pane still follows the tail (see reconcile).
|
|
125
|
+
followTail = next >= 0 && wasFollowing ? { offset: next, lineCount: state.lineCount() } : null;
|
|
126
|
+
debug.log('log.fold', 'fold-click-anchor', {
|
|
127
|
+
lineStart: target.kind === 'static' ? target.lineStart : null,
|
|
128
|
+
delta,
|
|
129
|
+
topBefore: heldTop,
|
|
130
|
+
scrollOffsetAfter: next,
|
|
131
|
+
maxScrollAfter: max,
|
|
132
|
+
followTail: followTail !== null,
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
reconcile() {
|
|
136
|
+
if (followTail === null) return;
|
|
137
|
+
// Someone else moved the pane (scroll keys, search, tail pin) — theirs.
|
|
138
|
+
if (state.getScrollOffset() !== followTail.offset) {
|
|
139
|
+
followTail = null;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (state.lineCount() <= followTail.lineCount) return;
|
|
143
|
+
debug.log('log.fold', 'fold-click-follow-tail', {
|
|
144
|
+
offset: followTail.offset,
|
|
145
|
+
lineCountBefore: followTail.lineCount,
|
|
146
|
+
lineCountNow: state.lineCount(),
|
|
147
|
+
});
|
|
148
|
+
followTail = null;
|
|
149
|
+
state.setScrollOffset(-1);
|
|
150
|
+
state.setFrozenTail(null);
|
|
151
|
+
},
|
|
152
|
+
};
|
|
46
153
|
}
|
|
47
154
|
|
|
48
155
|
export function createDashboardLogClickDeps(
|
|
@@ -97,6 +204,13 @@ export function createDashboardLogClickDeps(
|
|
|
97
204
|
? {
|
|
98
205
|
tryFoldToggle: (absIdx: number): boolean => {
|
|
99
206
|
const fold = deps.foldStack!;
|
|
207
|
+
const anchor = deps.foldClickAnchor;
|
|
208
|
+
// Screen frame of the click — lets the log tell «same spot again»
|
|
209
|
+
// from «a different row» (#81 had no way to).
|
|
210
|
+
const frame = anchor ? (() => {
|
|
211
|
+
const v = anchor.viewport();
|
|
212
|
+
return { screenRow: absIdx - v.top, top: v.top, scrollOffset: v.scrollOffset };
|
|
213
|
+
})() : {};
|
|
100
214
|
// Collapse from the first/last line of an expanded range or
|
|
101
215
|
// the row whose click expanded it (#16 — same spot again folds).
|
|
102
216
|
// Other lines of that range are rejected first so a body
|
|
@@ -114,6 +228,7 @@ export function createDashboardLogClickDeps(
|
|
|
114
228
|
reason: 'expanded-body',
|
|
115
229
|
lineStart: t.lineStart,
|
|
116
230
|
lineEnd: t.lineEnd,
|
|
231
|
+
...frame,
|
|
117
232
|
});
|
|
118
233
|
return false;
|
|
119
234
|
}
|
|
@@ -121,7 +236,8 @@ export function createDashboardLogClickDeps(
|
|
|
121
236
|
}
|
|
122
237
|
}
|
|
123
238
|
}
|
|
124
|
-
const
|
|
239
|
+
const toggle = (): boolean => fold.toggleAtLine(absIdx, frame);
|
|
240
|
+
const toggled = anchor ? anchor.run(toggle) : toggle();
|
|
125
241
|
if (toggled) deps.draw();
|
|
126
242
|
return toggled;
|
|
127
243
|
},
|