@pellux/goodvibes-tui 2.0.20 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +70 -0
- package/README.md +16 -16
- package/docs/README.md +4 -4
- package/docs/foundation-artifacts/operator-contract.json +59 -4
- package/package.json +16 -6
- package/src/cli/management.ts +5 -2
- package/src/cli/tui-startup.ts +34 -39
- package/src/core/agent-view-render.ts +157 -0
- package/src/core/approval-alert.ts +49 -11
- package/src/core/bookmark-navigation.ts +7 -1
- package/src/core/budget-breach-notifier.ts +27 -8
- package/src/core/compaction-receipt.ts +1 -1
- package/src/core/context-usage.ts +1 -1
- package/src/core/conversation-compaction-render.ts +3 -3
- package/src/core/conversation-display-only.ts +86 -0
- package/src/core/conversation-event-navigation.ts +27 -0
- package/src/core/conversation-line-cache.ts +216 -447
- package/src/core/conversation-render-context.ts +20 -27
- package/src/core/conversation-rendering.ts +157 -575
- package/src/core/conversation-splash-state.ts +6 -1
- package/src/core/conversation-types.ts +35 -0
- package/src/core/conversation-usage.ts +12 -0
- package/src/core/conversation.ts +130 -131
- package/src/core/long-task-notifier.ts +100 -31
- package/src/core/notices.ts +75 -0
- package/src/core/pairing-tailscale-gateway.ts +1 -1
- package/src/core/process-output-log.ts +143 -0
- package/src/core/rewind-receipt.ts +1 -1
- package/src/core/session-focus.ts +190 -0
- package/src/core/session-resume-core.ts +23 -81
- package/src/core/stream-event-wiring.ts +31 -5
- package/src/core/system-message-noise.ts +2 -2
- package/src/core/system-message-router.ts +16 -18
- package/src/core/turn-event-wiring.ts +272 -33
- package/src/core/work-tree-focus.ts +290 -0
- package/src/core/work-tree-fold-store.ts +143 -0
- package/src/core/work-tree-model.ts +799 -0
- package/src/core/work-tree-render.ts +384 -0
- package/src/core/work-tree-sources.ts +184 -0
- package/src/core/work-tree-wiring.ts +176 -0
- package/src/core/workstream-notification.ts +36 -6
- package/src/core/wrfc-notice-titles.ts +37 -0
- package/src/input/agents-modal-text.ts +119 -0
- package/src/input/agents-modal.ts +699 -0
- package/src/input/bookmark-modal.ts +33 -9
- package/src/input/changes-git.ts +251 -0
- package/src/input/changes-modal.ts +629 -0
- package/src/input/changes-model.ts +125 -0
- package/src/input/command-palette-types.ts +41 -0
- package/src/input/command-palette.ts +272 -0
- package/src/input/command-registry.ts +77 -35
- package/src/input/commands/acp-runtime.ts +19 -9
- package/src/input/commands/channel-runtime.ts +2 -2
- package/src/input/commands/ci-runtime.ts +1 -1
- package/src/input/commands/codebase-runtime.ts +1 -1
- package/src/input/commands/config.ts +7 -1
- package/src/input/commands/context-window.ts +25 -19
- package/src/input/commands/control-room-runtime.ts +18 -18
- package/src/input/commands/conversation-runtime.ts +0 -1
- package/src/input/commands/cost-runtime.ts +29 -20
- package/src/input/commands/diff-runtime.ts +35 -181
- package/src/input/commands/eval.ts +1 -1
- package/src/input/commands/experience-runtime.ts +3 -7
- package/src/input/commands/fresh-conversation.ts +86 -0
- package/src/input/commands/git-runtime.ts +5 -22
- package/src/input/commands/graph-runtime.ts +1 -1
- package/src/input/commands/guidance-runtime.ts +1 -1
- package/src/input/commands/health-runtime.ts +8 -9
- package/src/input/commands/hooks-runtime.ts +2 -2
- package/src/input/commands/hosted-runtime.ts +3 -3
- package/src/input/commands/incident-runtime.ts +3 -3
- package/src/input/commands/intelligence-runtime.ts +6 -9
- package/src/input/commands/knowledge.ts +2 -2
- package/src/input/commands/legacy-panel-command.ts +57 -0
- package/src/input/commands/local-auth-runtime.ts +8 -9
- package/src/input/commands/local-provider-runtime.ts +7 -4
- package/src/input/commands/local-runtime.ts +12 -15
- package/src/input/commands/local-setup-review.ts +1 -1
- package/src/input/commands/local-setup.ts +13 -19
- package/src/input/commands/memory.ts +1 -1
- package/src/input/commands/notify-runtime.ts +9 -0
- package/src/input/commands/operator-runtime.ts +10 -12
- package/src/input/commands/permissions-provenance.ts +3 -3
- package/src/input/commands/planning-runtime.ts +7 -7
- package/src/input/commands/platform-sandbox-runtime.ts +2 -2
- package/src/input/commands/platform-services-runtime.ts +1 -1
- package/src/input/commands/policy-dispatch.ts +2 -2
- package/src/input/commands/policy.ts +3 -3
- package/src/input/commands/provider.ts +1 -1
- package/src/input/commands/remote-runtime.ts +1 -1
- package/src/input/commands/review-runtime.ts +39 -94
- package/src/input/commands/rewind-runtime.ts +81 -108
- package/src/input/commands/runtime-services.ts +15 -17
- package/src/input/commands/services-runtime.ts +2 -2
- package/src/input/commands/session-content.ts +21 -5
- package/src/input/commands/session-workflow.ts +13 -38
- package/src/input/commands/settings-sync-runtime.ts +2 -2
- package/src/input/commands/shell-core.ts +19 -4
- package/src/input/commands/skills-runtime.ts +2 -2
- package/src/input/commands/status-runtime.ts +13 -0
- package/src/input/commands/subscription-runtime.ts +1 -1
- package/src/input/commands/tasks-runtime.ts +2 -8
- package/src/input/commands/work-plan-runtime.ts +4 -4
- package/src/input/commands/workstream-attempts.ts +24 -47
- package/src/input/commands/workstream-runtime.ts +2 -2
- package/src/input/commands/worktree-runtime.ts +3 -3
- package/src/input/commands.ts +3 -1
- package/src/input/composer-editor.ts +44 -0
- package/src/input/concealed-input.ts +1 -1
- package/src/input/config-modal-types.ts +20 -8
- package/src/input/config-modal.ts +106 -105
- package/src/input/confirm-dialog-types.ts +28 -0
- package/src/input/confirm-dialog.ts +70 -0
- package/src/input/delete-key-policy.ts +3 -3
- package/src/input/feed-context-factory.ts +15 -16
- package/src/input/handler-command-route.ts +7 -42
- package/src/input/handler-content-actions.ts +16 -15
- package/src/input/handler-feed-routes.ts +26 -211
- package/src/input/handler-feed.ts +68 -95
- package/src/input/handler-interactions.ts +5 -3
- package/src/input/handler-modal-routes.ts +215 -216
- package/src/input/handler-modal-stack.ts +81 -11
- package/src/input/handler-modal-token-routes.ts +4 -0
- package/src/input/handler-onboarding.ts +0 -1
- package/src/input/handler-picker-routes.ts +65 -107
- package/src/input/handler-session-view-route.ts +149 -0
- package/src/input/handler-shortcuts.ts +16 -154
- package/src/input/handler-types.ts +4 -2
- package/src/input/handler-ui-state.ts +45 -15
- package/src/input/handler-work-tree-route.ts +123 -0
- package/src/input/handler.ts +25 -70
- package/src/input/keybindings.ts +16 -71
- package/src/input/masked-entry-modal.ts +117 -0
- package/src/input/mcp-workspace.ts +48 -17
- package/src/input/model-picker-filter.ts +8 -0
- package/src/input/model-picker-open.ts +99 -0
- package/src/input/model-picker-types.ts +1 -1
- package/src/input/model-picker.ts +92 -9
- package/src/input/notifications-modal.ts +123 -0
- package/src/input/overlay-filter.ts +26 -0
- package/src/input/profile-picker-modal.ts +37 -8
- package/src/input/search.ts +3 -3
- package/src/input/selection-modal-queue.ts +2 -0
- package/src/input/selection-modal.ts +28 -16
- package/src/input/session-picker-modal.ts +33 -8
- package/src/input/settings-modal-activation.ts +7 -2
- package/src/input/settings-modal-data.ts +11 -6
- package/src/input/settings-modal-types.ts +1 -1
- package/src/input/settings-modal.ts +20 -20
- package/src/input/submission-intent.ts +1 -1
- package/src/input/submission-router.ts +4 -9
- package/src/input/surface-modal-host.ts +110 -0
- package/src/input/theme-settings-actions.ts +82 -0
- package/src/input/usage-modal.ts +183 -0
- package/src/input/views.ts +97 -0
- package/src/main.ts +175 -165
- package/src/permissions/hunk-selection.ts +1 -1
- package/src/permissions/prompt-card.ts +511 -0
- package/src/permissions/prompt.ts +59 -622
- package/src/renderer/agents-modal.ts +310 -0
- package/src/renderer/autocomplete-overlay.ts +42 -220
- package/src/renderer/back-to-bottom.ts +80 -0
- package/src/renderer/block-actions-overlay.ts +30 -48
- package/src/renderer/bookmark-modal.ts +48 -79
- package/src/renderer/changes-modal.ts +325 -0
- package/src/renderer/code-block.ts +184 -106
- package/src/renderer/command-palette.ts +93 -0
- package/src/renderer/composer-fragments.ts +4 -4
- package/src/renderer/composer.ts +146 -0
- package/src/renderer/compositor.ts +26 -160
- package/src/renderer/config-modal.ts +185 -81
- package/src/renderer/confirm-dialog.ts +50 -0
- package/src/renderer/context-inspector.ts +116 -154
- package/src/renderer/context-status-hint.ts +4 -4
- package/src/renderer/conversation-layout.ts +20 -0
- package/src/renderer/conversation-overlays.ts +94 -84
- package/src/renderer/conversation-surface.ts +19 -38
- package/src/renderer/diff-tint.ts +98 -0
- package/src/renderer/file-picker-overlay.ts +63 -90
- package/src/renderer/footer-targets.ts +38 -0
- package/src/renderer/git-status.ts +3 -4
- package/src/renderer/header-line.ts +140 -117
- package/src/renderer/help-overlay.ts +133 -219
- package/src/renderer/history-search-overlay.ts +15 -12
- package/src/renderer/lane-graph/bead.ts +800 -0
- package/src/renderer/lane-graph/glyphs.ts +134 -0
- package/src/renderer/lane-graph/layout.ts +248 -0
- package/src/renderer/lane-graph/paint.ts +537 -0
- package/src/renderer/lane-graph/semantic-memo.ts +77 -0
- package/src/renderer/layout-engine.ts +2 -57
- package/src/renderer/layout.ts +7 -13
- package/src/renderer/markdown-inline.ts +2 -1
- package/src/renderer/markdown-table.ts +17 -12
- package/src/renderer/markdown.ts +92 -34
- package/src/renderer/masked-entry-modal.ts +78 -0
- package/src/renderer/mcp-workspace.ts +166 -203
- package/src/renderer/model-workspace.ts +381 -436
- package/src/renderer/notifications-modal.ts +116 -0
- package/src/renderer/onboarding/onboarding-wizard.ts +178 -588
- package/src/renderer/process-view.ts +192 -0
- package/src/renderer/profile-picker-modal.ts +58 -110
- package/src/renderer/search-overlay.ts +6 -3
- package/src/renderer/selection-modal-overlay.ts +87 -327
- package/src/renderer/semantic-diff.ts +2 -2
- package/src/renderer/session-chips.ts +102 -0
- package/src/renderer/session-picker-modal.ts +177 -297
- package/src/renderer/settings-modal-connections.ts +14 -23
- package/src/renderer/settings-modal-context.ts +367 -0
- package/src/renderer/settings-modal-helpers.ts +6 -11
- package/src/renderer/settings-modal.ts +322 -726
- package/src/renderer/shell-surface.ts +277 -175
- package/src/renderer/shortcuts-overlay.ts +217 -0
- package/src/renderer/status-line.ts +475 -0
- package/src/renderer/surface-compose.ts +174 -0
- package/src/renderer/surface-kit-extra.ts +303 -0
- package/src/renderer/surface-kit-list.ts +366 -0
- package/src/renderer/surface-kit-parts.ts +360 -0
- package/src/renderer/surface-kit.ts +568 -0
- package/src/renderer/syntax-highlighter.ts +280 -197
- package/src/renderer/syntax-theme.ts +51 -0
- package/src/renderer/system-message.ts +0 -1
- package/src/renderer/term-caps.ts +42 -1
- package/src/renderer/terminal-bg-probe.ts +116 -19
- package/src/renderer/terminal-palette-probe.ts +322 -0
- package/src/renderer/terminal-palette-reader.ts +183 -0
- package/src/renderer/terminal-palette.ts +58 -0
- package/src/renderer/theme-mode-config.ts +16 -1
- package/src/renderer/theme.ts +234 -207
- package/src/renderer/thinking.ts +0 -1
- package/src/renderer/throbber.ts +188 -0
- package/src/renderer/toast-center.ts +119 -0
- package/src/renderer/ui-factory.ts +39 -602
- package/src/renderer/ui-primitives.ts +8 -25
- package/src/renderer/usage-modal.ts +353 -0
- package/src/renderer/user-message.ts +58 -0
- package/src/renderer/voice-capture-chip.ts +53 -0
- package/src/runtime/bootstrap-command-context.ts +0 -5
- package/src/runtime/bootstrap-command-parts.ts +38 -89
- package/src/runtime/bootstrap-core.ts +13 -15
- package/src/runtime/bootstrap-hook-bridge.ts +12 -21
- package/src/runtime/bootstrap-notifier-sync.ts +17 -1
- package/src/runtime/bootstrap-shell.ts +16 -50
- package/src/runtime/bootstrap.ts +14 -14
- package/src/runtime/client/fleet-union.ts +5 -5
- package/src/runtime/client/hosted-exit.ts +2 -2
- package/src/runtime/client/hosted-sessions.ts +1 -1
- package/src/runtime/client/spine-adoption.ts +1 -1
- package/src/runtime/context-accounting-source.ts +5 -4
- package/src/runtime/index.ts +1 -0
- package/src/runtime/notification-dispatch.ts +102 -29
- package/src/runtime/recovery-offer-wiring.ts +4 -1
- package/src/runtime/recovery-prompt.ts +31 -15
- package/src/runtime/runtime-services-types.ts +10 -6
- package/src/runtime/services.ts +17 -12
- package/src/runtime/session-continuity-hints.ts +2 -5
- package/src/runtime/session-inbound-inputs.ts +1 -1
- package/src/runtime/session-resume-liveness-confirm.ts +14 -2
- package/src/runtime/startup-typeahead-gate.ts +103 -0
- package/src/runtime/store/domains/domain-read-matrix.ts +0 -1
- package/src/runtime/store/domains/index.ts +0 -8
- package/src/runtime/store/selectors/index.ts +0 -28
- package/src/runtime/store/state.ts +0 -8
- package/src/runtime/ui-services.ts +4 -6
- package/src/runtime/usage-tracker.ts +348 -0
- package/src/runtime/wrfc-persistence.ts +4 -4
- package/src/shell/blocking-input.ts +92 -2
- package/src/shell/recovery-input-helpers.ts +1 -1
- package/src/shell/session-views.ts +539 -0
- package/src/shell/status-report.ts +86 -0
- package/src/shell/transcript-scroll.ts +68 -0
- package/src/shell/ui-openers.ts +84 -172
- package/src/shell/view-openers.ts +205 -0
- package/src/tools/wrfc-agent-guard.ts +22 -2
- package/src/utils/format-duration.ts +4 -8
- package/src/utils/splash-lines.ts +9 -1
- package/src/utils/terminal-width.ts +56 -0
- package/src/verification/verification-ledger.ts +18 -16
- package/src/{panels → views}/agent-inspector-shared.ts +2 -2
- package/src/{panels → views}/builtin-modals.ts +22 -21
- package/src/views/builtin-views.ts +135 -0
- package/src/{panels → views}/confirm-state.ts +9 -9
- package/src/{panels → views}/diff-review-model.ts +10 -3
- package/src/{panels → views}/eval-registry.ts +1 -1
- package/src/{panels → views}/fleet-acts.ts +49 -85
- package/src/{panels/fleet-panel-format.ts → views/fleet-format.ts} +40 -40
- package/src/{panels → views}/fleet-gateway.ts +5 -5
- package/src/{panels → views}/fleet-observed-render.ts +16 -16
- package/src/{panels → views}/fleet-read-model.ts +9 -9
- package/src/{panels/fleet-tabs.ts → views/fleet-session-tabs.ts} +13 -13
- package/src/{panels → views}/fleet-spawn.ts +33 -49
- package/src/{panels → views}/fleet-steer.ts +12 -12
- package/src/{panels → views}/fleet-stop.ts +4 -4
- package/src/{panels → views}/fleet-transcript.ts +26 -27
- package/src/{panels/fleet-panel-worktree-detail.ts → views/fleet-worktree-detail.ts} +7 -7
- package/src/{panels → views}/hosted-session-feed.ts +6 -6
- package/src/views/modal-surface-registry.ts +41 -0
- package/src/{panels → views}/modals/devices-modal.ts +2 -2
- package/src/{panels → views}/modals/hooks-modal.ts +13 -13
- package/src/{panels → views}/modals/keybindings-modal.ts +19 -27
- package/src/{panels → views}/modals/knowledge-modal.ts +6 -5
- package/src/{panels → views}/modals/local-auth-modal.ts +22 -21
- package/src/{panels → views}/modals/marketplace-modal.ts +11 -11
- package/src/{panels → views}/modals/memory-modal.ts +8 -8
- package/src/{panels → views}/modals/modal-surface-helpers.ts +15 -5
- package/src/{panels → views}/modals/pairing-modal.ts +9 -9
- package/src/{panels → views}/modals/planning-modal.ts +15 -20
- package/src/{panels → views}/modals/plugins-modal.ts +6 -6
- package/src/{panels → views}/modals/policy-modal.ts +13 -5
- package/src/{panels → views}/modals/provider-health-modal.ts +4 -4
- package/src/{panels → views}/modals/remote-modal.ts +3 -3
- package/src/{panels → views}/modals/sandbox-modal.ts +4 -4
- package/src/{panels → views}/modals/security-modal.ts +22 -16
- package/src/{panels → views}/modals/services-modal.ts +3 -3
- package/src/{panels → views}/modals/settings-sync-modal.ts +4 -4
- package/src/{panels → views}/modals/skills-modal.ts +9 -9
- package/src/{panels → views}/modals/subscription-modal.ts +3 -3
- package/src/{panels → views}/modals/work-plan-modal.ts +10 -13
- package/src/views/notifications-feed.ts +314 -0
- package/src/{panels → views}/polish-core.ts +38 -36
- package/src/{panels → views}/polish-tables.ts +35 -26
- package/src/{panels → views}/polish.ts +101 -95
- package/src/{panels → views}/project-planning-answer-actions.ts +3 -3
- package/src/{panels → views}/provider-health-routes.ts +14 -14
- package/src/{panels → views}/search-focus.ts +8 -8
- package/src/{panels → views}/session-maintenance.ts +11 -11
- package/src/views/skills-discovery.ts +159 -0
- package/src/views/view-deps.ts +80 -0
- package/src/views/view-panel-adapter.ts +53 -0
- package/src/{panels → views}/workstream-graph-render.ts +1 -1
- package/src/core/conversation-fold.ts +0 -101
- package/src/core/conversation-turn-structure.ts +0 -464
- package/src/core/conversation-user-receipts.ts +0 -42
- package/src/input/commands/operator-panel-runtime.ts +0 -186
- package/src/input/panel-integration-actions.ts +0 -33
- package/src/input/panel-mouse-geometry.ts +0 -97
- package/src/input/panel-paste-flood-guard.ts +0 -86
- package/src/panels/base-panel.ts +0 -301
- package/src/panels/builtin/agent.ts +0 -27
- package/src/panels/builtin/development.ts +0 -92
- package/src/panels/builtin/knowledge.ts +0 -12
- package/src/panels/builtin/operations.ts +0 -177
- package/src/panels/builtin/session.ts +0 -81
- package/src/panels/builtin/shared.ts +0 -357
- package/src/panels/builtin-panels.ts +0 -28
- package/src/panels/cost-tracker-panel.ts +0 -746
- package/src/panels/diff-panel.ts +0 -797
- package/src/panels/diff-review-panel.ts +0 -395
- package/src/panels/expandable-list-panel.ts +0 -189
- package/src/panels/fleet-deep-link.ts +0 -31
- package/src/panels/fleet-panel.ts +0 -799
- package/src/panels/git-panel.ts +0 -800
- package/src/panels/hosted-session-panel.ts +0 -221
- package/src/panels/index.ts +0 -21
- package/src/panels/local-auth-panel.ts +0 -478
- package/src/panels/notifications-feed.ts +0 -127
- package/src/panels/notifications-panel.ts +0 -160
- package/src/panels/panel-confirm-overlay.ts +0 -72
- package/src/panels/panel-manager.ts +0 -791
- package/src/panels/plugins-panel.ts +0 -371
- package/src/panels/scrollable-list-panel.ts +0 -498
- package/src/panels/skills-panel.ts +0 -454
- package/src/panels/token-budget-panel.ts +0 -688
- package/src/panels/types.ts +0 -191
- package/src/panels/wrfc-panel-format.ts +0 -133
- package/src/renderer/diff-view.ts +0 -134
- package/src/renderer/fleet-tab-strip.ts +0 -56
- package/src/renderer/footer-tips.ts +0 -53
- package/src/renderer/fullscreen-primitives.ts +0 -146
- package/src/renderer/fullscreen-workspace.ts +0 -194
- package/src/renderer/hint-grammar.ts +0 -52
- package/src/renderer/modal-factory.ts +0 -480
- package/src/renderer/overlay-box.ts +0 -154
- package/src/renderer/overlay-viewport.ts +0 -104
- package/src/renderer/panel-composite.ts +0 -145
- package/src/renderer/panel-workspace-bar.ts +0 -54
- package/src/renderer/process-indicator.ts +0 -163
- package/src/renderer/tab-strip.ts +0 -222
- package/src/renderer/text-layout.ts +0 -31
- package/src/renderer/tool-call.ts +0 -276
- package/src/renderer/tool-result-expanded-lines.ts +0 -48
- package/src/renderer/tool-result-summary.ts +0 -219
- package/src/runtime/diagnostics/panels/index.ts +0 -21
- package/src/runtime/diagnostics/panels/ops.ts +0 -156
- package/src/runtime/perf/panel-contracts.ts +0 -32
- package/src/runtime/store/domains/panels.ts +0 -117
- /package/src/{panels → views}/memory-consolidation-gateway.ts +0 -0
- /package/src/{panels → views}/modals/modal-theme.ts +0 -0
- /package/src/{panels → views}/provider-health-domains.ts +0 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* work-tree-focus.ts, moving the keyboard through the work tree.
|
|
3
|
+
*
|
|
4
|
+
* The navigable rows are the blocks the work tree registered with a
|
|
5
|
+
* `workTree` field: turn headers, beads, and agent lanes (their spawn or
|
|
6
|
+
* folded row), in screen order. Keys:
|
|
7
|
+
*
|
|
8
|
+
* ↑ ↓ previous / next row
|
|
9
|
+
* ← fold: close an open body; on a lane's bead or spawn row fold the
|
|
10
|
+
* lane to one ◉; on the spine fold the turn to its header
|
|
11
|
+
* → unfold: open a body (a second → shows the rest of a capped
|
|
12
|
+
* body); unfold a folded lane or turn
|
|
13
|
+
* Enter open or close a bead's body; unfold a folded lane; fold or
|
|
14
|
+
* unfold a turn
|
|
15
|
+
*
|
|
16
|
+
* Pure: reads the registry and collapse state, returns the collapse changes
|
|
17
|
+
* and the row to focus next. Nothing here stops, cancels or sends anything.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { BlockMeta } from './conversation-types.ts';
|
|
21
|
+
import type { TurnOutcome } from './work-tree-sources.ts';
|
|
22
|
+
|
|
23
|
+
/** A turn's saved ending: the user message it answered (by index and fingerprint) and how it ended. */
|
|
24
|
+
export interface TurnOutcomeRecord {
|
|
25
|
+
readonly index: number;
|
|
26
|
+
readonly fingerprint: string;
|
|
27
|
+
readonly outcome: TurnOutcome;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Most turn endings kept per session. */
|
|
31
|
+
export const MAX_TURN_OUTCOMES = 500;
|
|
32
|
+
|
|
33
|
+
export type NavBlock = BlockMeta & { readonly workTree: NonNullable<BlockMeta['workTree']> };
|
|
34
|
+
|
|
35
|
+
export interface FocusChange {
|
|
36
|
+
/** Collapse keys to set (true = folded / closed). */
|
|
37
|
+
readonly set: ReadonlyArray<readonly [string, boolean]>;
|
|
38
|
+
/** The row to focus afterwards. */
|
|
39
|
+
readonly focus: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Navigable rows in screen order. */
|
|
43
|
+
export function navigableRows(blocks: readonly BlockMeta[]): NavBlock[] {
|
|
44
|
+
return blocks.filter((b): b is NavBlock => b.workTree !== undefined).sort((a, b) => a.startLine - b.startLine);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function findRow(blocks: readonly BlockMeta[], id: string): NavBlock | undefined {
|
|
48
|
+
return navigableRows(blocks).find((b) => b.workTree.id === id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The row to focus on entering the work tree: the last one at or above `anchorLine` (else the last row). */
|
|
52
|
+
export function initialFocus(blocks: readonly BlockMeta[], anchorLine: number): string | null {
|
|
53
|
+
const rows = navigableRows(blocks);
|
|
54
|
+
if (rows.length === 0) return null;
|
|
55
|
+
const above = rows.filter((b) => b.startLine <= anchorLine);
|
|
56
|
+
return (above[above.length - 1] ?? rows[rows.length - 1]!).workTree.id;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The row `delta` rows from `current` (clamped); null when there are none. */
|
|
60
|
+
export function moveFocus(blocks: readonly BlockMeta[], current: string | null, delta: number): string | null {
|
|
61
|
+
const rows = navigableRows(blocks);
|
|
62
|
+
if (rows.length === 0) return null;
|
|
63
|
+
const at = current === null ? -1 : rows.findIndex((b) => b.workTree.id === current);
|
|
64
|
+
if (at < 0) return rows[delta < 0 ? rows.length - 1 : 0]!.workTree.id;
|
|
65
|
+
return rows[Math.max(0, Math.min(rows.length - 1, at + delta))]!.workTree.id;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The turn header a row belongs to (the nearest turn row above it). */
|
|
69
|
+
function turnOf(blocks: readonly BlockMeta[], row: NavBlock): NavBlock | undefined {
|
|
70
|
+
const rows = navigableRows(blocks).filter((b) => b.workTree.kind === 'turn' && b.startLine <= row.startLine);
|
|
71
|
+
return rows[rows.length - 1];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function laneRow(blocks: readonly BlockMeta[], laneKey: string): NavBlock | undefined {
|
|
75
|
+
return navigableRows(blocks).find((b) => b.workTree.kind === 'lane' && b.workTree.laneKey === laneKey);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** ← on the focused row. */
|
|
79
|
+
export function foldFocused(blocks: readonly BlockMeta[], id: string): FocusChange | null {
|
|
80
|
+
const row = findRow(blocks, id);
|
|
81
|
+
if (!row) return null;
|
|
82
|
+
const wt = row.workTree;
|
|
83
|
+
if (wt.kind === 'bead') {
|
|
84
|
+
if (wt.open) return { set: [[row.collapseKey, true], ...(wt.moreKey ? [[wt.moreKey, false] as const] : [])], focus: id };
|
|
85
|
+
if (wt.laneKey) {
|
|
86
|
+
const lane = laneRow(blocks, wt.laneKey);
|
|
87
|
+
return { set: [[wt.laneKey, true]], focus: lane?.workTree.id ?? wt.laneKey };
|
|
88
|
+
}
|
|
89
|
+
const turn = turnOf(blocks, row);
|
|
90
|
+
return turn ? { set: [[turn.collapseKey, true]], focus: turn.workTree.id } : null;
|
|
91
|
+
}
|
|
92
|
+
if (wt.kind === 'lane') {
|
|
93
|
+
if (wt.open) return { set: [[row.collapseKey, true]], focus: id };
|
|
94
|
+
const turn = turnOf(blocks, row);
|
|
95
|
+
return turn ? { set: [[turn.collapseKey, true]], focus: turn.workTree.id } : null;
|
|
96
|
+
}
|
|
97
|
+
return wt.open ? { set: [[row.collapseKey, true]], focus: id } : null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** → on the focused row. */
|
|
101
|
+
export function unfoldFocused(blocks: readonly BlockMeta[], id: string): FocusChange | null {
|
|
102
|
+
const row = findRow(blocks, id);
|
|
103
|
+
if (!row) return null;
|
|
104
|
+
const wt = row.workTree;
|
|
105
|
+
if (wt.kind === 'bead') {
|
|
106
|
+
if (!wt.hasBody) return null;
|
|
107
|
+
if (!wt.open) return { set: [[row.collapseKey, false]], focus: id };
|
|
108
|
+
if (wt.capped && wt.moreKey) return { set: [[wt.moreKey, true]], focus: id };
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
return wt.open ? null : { set: [[row.collapseKey, false]], focus: id };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Enter on the focused row. */
|
|
115
|
+
export function activateFocused(blocks: readonly BlockMeta[], id: string): FocusChange | null {
|
|
116
|
+
const row = findRow(blocks, id);
|
|
117
|
+
if (!row) return null;
|
|
118
|
+
const wt = row.workTree;
|
|
119
|
+
if (wt.kind === 'bead') {
|
|
120
|
+
if (!wt.hasBody) return null;
|
|
121
|
+
return wt.open ? { set: [[row.collapseKey, true]], focus: id } : { set: [[row.collapseKey, false]], focus: id };
|
|
122
|
+
}
|
|
123
|
+
if (wt.kind === 'lane') return wt.open ? null : { set: [[row.collapseKey, false]], focus: id };
|
|
124
|
+
return { set: [[row.collapseKey, wt.open]], focus: id };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface WorkTreeControllerDeps {
|
|
128
|
+
/** The current block registry (flushed). */
|
|
129
|
+
readonly blocks: () => readonly BlockMeta[];
|
|
130
|
+
readonly collapseState: () => Map<string, boolean>;
|
|
131
|
+
readonly markDirty: () => void;
|
|
132
|
+
/** Exempts a key from search's close-time auto re-collapse (a deliberate user act). */
|
|
133
|
+
readonly noteUserTouch: (key: string) => void;
|
|
134
|
+
/** Whether the last build drew something live. */
|
|
135
|
+
readonly drewLive: () => boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Collapse keys the work tree owns (work-tree-model.ts). */
|
|
139
|
+
const FOLD_KEY = /^(turn_\d+|bead_|beadmore_|lane_)/;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The work tree's keyboard focus, live repaint tick and fold state, owned by a
|
|
143
|
+
* ConversationManager (as `conversation.workTree`).
|
|
144
|
+
*/
|
|
145
|
+
export class WorkTreeController {
|
|
146
|
+
private focusId: string | null = null;
|
|
147
|
+
private liveFrame = 0;
|
|
148
|
+
private foldListener: (() => void) | null = null;
|
|
149
|
+
/** Failed and cancelled turn endings of this session, by user message index. */
|
|
150
|
+
private readonly outcomes = new Map<number, TurnOutcomeRecord>();
|
|
151
|
+
private readonly resetListeners = new Set<() => void>();
|
|
152
|
+
|
|
153
|
+
constructor(private readonly deps: WorkTreeControllerDeps) {}
|
|
154
|
+
|
|
155
|
+
/** The focused row id, or null while the keyboard is in the composer. */
|
|
156
|
+
get focus(): string | null { return this.focusId; }
|
|
157
|
+
/** Spinner frame for running beads. */
|
|
158
|
+
get frame(): number { return this.liveFrame; }
|
|
159
|
+
get focused(): boolean { return this.focusId !== null; }
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The transcript was reset or replaced wholesale (a new or resumed session):
|
|
163
|
+
* focus, turn endings and anything keyed by message index belong to the old
|
|
164
|
+
* transcript and go.
|
|
165
|
+
*/
|
|
166
|
+
reset(): void {
|
|
167
|
+
this.focusId = null;
|
|
168
|
+
this.outcomes.clear();
|
|
169
|
+
for (const listener of this.resetListeners) listener();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Be told when the transcript is reset (the timing store keys turns by message index). */
|
|
173
|
+
onReset(listener: () => void): () => void {
|
|
174
|
+
this.resetListeners.add(listener);
|
|
175
|
+
return () => { this.resetListeners.delete(listener); };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Record how a turn ended. Only failed and cancelled endings are kept (a
|
|
180
|
+
* completed turn is the default and says nothing extra). Returns whether the
|
|
181
|
+
* kept records changed.
|
|
182
|
+
*/
|
|
183
|
+
recordTurnOutcome(record: TurnOutcomeRecord): boolean {
|
|
184
|
+
const had = this.outcomes.delete(record.index);
|
|
185
|
+
if (record.outcome === 'completed') return had;
|
|
186
|
+
this.outcomes.set(record.index, record);
|
|
187
|
+
while (this.outcomes.size > MAX_TURN_OUTCOMES) {
|
|
188
|
+
const oldest = this.outcomes.keys().next();
|
|
189
|
+
if (oldest.done) break;
|
|
190
|
+
this.outcomes.delete(oldest.value);
|
|
191
|
+
}
|
|
192
|
+
this.deps.markDirty();
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The recorded ending of the turn answering the user message at `index`. */
|
|
197
|
+
turnOutcome(index: number): TurnOutcomeRecord | undefined {
|
|
198
|
+
return this.outcomes.get(index);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Every recorded turn ending, oldest first (the session sidecar keeps them). */
|
|
202
|
+
turnOutcomes(): TurnOutcomeRecord[] {
|
|
203
|
+
return [...this.outcomes.values()];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Restore saved turn endings (a resumed session keeps its failed and cancelled headers). */
|
|
207
|
+
restoreTurnOutcomes(records: Iterable<TurnOutcomeRecord>): void {
|
|
208
|
+
for (const record of records) this.recordTurnOutcome(record);
|
|
209
|
+
this.deps.markDirty();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Something the work tree draws changed outside the transcript (a ◈ summary landed): rebuild. */
|
|
213
|
+
invalidate(): void { this.deps.markDirty(); }
|
|
214
|
+
|
|
215
|
+
/** Be told when a fold decision changes (the session keeps them in its sidecar). */
|
|
216
|
+
onFoldChange(listener: (() => void) | null): void { this.foldListener = listener; }
|
|
217
|
+
notifyFoldChange(): void { this.foldListener?.(); }
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Repaint live rows as time passes (a running bead spins, its time counts
|
|
221
|
+
* up). Call before each frame; it marks the transcript dirty only while the
|
|
222
|
+
* last build drew something live and the frame moved on.
|
|
223
|
+
*/
|
|
224
|
+
tickLive(now = Date.now()): void {
|
|
225
|
+
const frame = Math.floor(now / 150);
|
|
226
|
+
if (frame === this.liveFrame) return;
|
|
227
|
+
this.liveFrame = frame;
|
|
228
|
+
if (this.deps.drewLive()) this.deps.markDirty();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Enter the work tree at the last row at or above `anchorLine`; false when there is nothing to focus. */
|
|
232
|
+
enter(anchorLine: number): boolean {
|
|
233
|
+
const id = initialFocus(this.deps.blocks(), anchorLine);
|
|
234
|
+
if (id === null) return false;
|
|
235
|
+
this.focusId = id;
|
|
236
|
+
this.deps.markDirty();
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Leave the work tree (Esc). Never stops or cancels anything. */
|
|
241
|
+
leave(): void {
|
|
242
|
+
if (this.focusId === null) return;
|
|
243
|
+
this.focusId = null;
|
|
244
|
+
this.deps.markDirty();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
move(delta: number): boolean {
|
|
248
|
+
const next = moveFocus(this.deps.blocks(), this.focusId, delta);
|
|
249
|
+
if (next === null || next === this.focusId) return false;
|
|
250
|
+
this.focusId = next;
|
|
251
|
+
this.deps.markDirty();
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** ← (fold), → (unfold) or Enter (activate) on the focused row. */
|
|
256
|
+
act(action: 'fold' | 'unfold' | 'activate'): boolean {
|
|
257
|
+
if (this.focusId === null) return false;
|
|
258
|
+
const blocks = this.deps.blocks();
|
|
259
|
+
const change = action === 'fold' ? foldFocused(blocks, this.focusId)
|
|
260
|
+
: action === 'unfold' ? unfoldFocused(blocks, this.focusId)
|
|
261
|
+
: activateFocused(blocks, this.focusId);
|
|
262
|
+
if (!change) return false;
|
|
263
|
+
const collapse = this.deps.collapseState();
|
|
264
|
+
for (const [key, value] of change.set) {
|
|
265
|
+
collapse.set(key, value);
|
|
266
|
+
this.deps.noteUserTouch(key);
|
|
267
|
+
}
|
|
268
|
+
this.focusId = change.focus;
|
|
269
|
+
this.deps.markDirty();
|
|
270
|
+
this.foldListener?.();
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** The focused row's block (copy, save and bookmark act on it). */
|
|
275
|
+
focusBlock(): BlockMeta | null {
|
|
276
|
+
return this.focusId === null ? null : findRow(this.deps.blocks(), this.focusId) ?? null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Every work-tree fold decision (turns, lanes, open beads), oldest first. */
|
|
280
|
+
foldState(): Array<[string, boolean]> {
|
|
281
|
+
return [...this.deps.collapseState()].filter(([key]) => FOLD_KEY.test(key));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Restore saved fold decisions (a resumed session keeps its view). */
|
|
285
|
+
restoreFoldState(entries: Iterable<readonly [string, boolean]>): void {
|
|
286
|
+
const collapse = this.deps.collapseState();
|
|
287
|
+
for (const [key, value] of entries) if (FOLD_KEY.test(key)) collapse.set(key, value);
|
|
288
|
+
this.deps.markDirty();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* work-tree-fold-store.ts, the work tree's fold state, kept per session.
|
|
3
|
+
*
|
|
4
|
+
* Which turns are folded, which agent lanes are folded, which beads are open
|
|
5
|
+
* and which opened bodies show everything, plus which turns failed or were
|
|
6
|
+
* cancelled (keyed by their user message's index and fingerprint, so a
|
|
7
|
+
* resumed header still says so): kept beside the session file as
|
|
8
|
+
* `<stem>.work-tree.json` (the same stem rule the session file uses, see
|
|
9
|
+
* sanitizeSessionName), so reopening a session keeps the view.
|
|
10
|
+
*
|
|
11
|
+
* Bounded (the most recent MAX_KEYS decisions), validated on load (anything
|
|
12
|
+
* that is not a known key shape with a boolean value is dropped), written
|
|
13
|
+
* atomically (temp file + rename), and swept: a sidecar whose session file is
|
|
14
|
+
* gone is removed by sweepOrphanWorkTreeFolds.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { sanitizeSessionName } from '@pellux/goodvibes-sdk/platform/sessions';
|
|
20
|
+
import { MAX_TURN_OUTCOMES, type TurnOutcomeRecord } from './work-tree-focus.ts';
|
|
21
|
+
|
|
22
|
+
const SUFFIX = '.work-tree.json';
|
|
23
|
+
const VERSION = 1;
|
|
24
|
+
/** Most fold decisions kept per session. */
|
|
25
|
+
export const MAX_FOLD_KEYS = 2000;
|
|
26
|
+
/** A staging file older than this is crash residue. */
|
|
27
|
+
const TMP_MAX_AGE_MS = 60 * 60 * 1000;
|
|
28
|
+
|
|
29
|
+
/** Collapse keys the work tree owns (work-tree-model.ts). */
|
|
30
|
+
const KEY_SHAPE = /^(turn_\d+|bead_[\w:./-]+|beadmore_[\w:./-]+|lane_[\w:./-]+)$/;
|
|
31
|
+
|
|
32
|
+
export function isWorkTreeFoldKey(key: string): boolean {
|
|
33
|
+
return KEY_SHAPE.test(key);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function sidecarPath(sessionsDir: string, sessionId: string): string | null {
|
|
37
|
+
if (!sessionsDir || !sessionId) return null;
|
|
38
|
+
const stem = sanitizeSessionName(sessionId);
|
|
39
|
+
if (stem === '') return null;
|
|
40
|
+
return join(sessionsDir, `${stem}${SUFFIX}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Keep only work-tree keys with boolean values, the most recent MAX_FOLD_KEYS. */
|
|
44
|
+
export function boundFoldState(entries: Iterable<readonly [string, unknown]>): Array<[string, boolean]> {
|
|
45
|
+
const out: Array<[string, boolean]> = [];
|
|
46
|
+
for (const [key, value] of entries) if (typeof value === 'boolean' && isWorkTreeFoldKey(key)) out.push([key, value]);
|
|
47
|
+
return out.length > MAX_FOLD_KEYS ? out.slice(out.length - MAX_FOLD_KEYS) : out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Keep only well-formed failed/cancelled turn endings, the most recent MAX_TURN_OUTCOMES. */
|
|
51
|
+
export function boundTurnOutcomes(records: Iterable<unknown>): TurnOutcomeRecord[] {
|
|
52
|
+
const out: TurnOutcomeRecord[] = [];
|
|
53
|
+
for (const record of records) {
|
|
54
|
+
if (!record || typeof record !== 'object') continue;
|
|
55
|
+
const r = record as Record<string, unknown>;
|
|
56
|
+
if (typeof r.index !== 'number' || !Number.isInteger(r.index) || r.index < 0) continue;
|
|
57
|
+
if (typeof r.fingerprint !== 'string' || r.fingerprint.length === 0 || r.fingerprint.length > 200) continue;
|
|
58
|
+
if (r.outcome !== 'failed' && r.outcome !== 'cancelled') continue;
|
|
59
|
+
out.push({ index: r.index, fingerprint: r.fingerprint, outcome: r.outcome });
|
|
60
|
+
}
|
|
61
|
+
return out.length > MAX_TURN_OUTCOMES ? out.slice(out.length - MAX_TURN_OUTCOMES) : out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Write the session's fold state and its failed/cancelled turn endings.
|
|
66
|
+
* Best effort: a failed write never breaks the session.
|
|
67
|
+
*/
|
|
68
|
+
export function saveWorkTreeFolds(sessionsDir: string, sessionId: string, state: Iterable<readonly [string, boolean]>, turns: Iterable<TurnOutcomeRecord> = []): void {
|
|
69
|
+
const path = sidecarPath(sessionsDir, sessionId);
|
|
70
|
+
if (!path) return;
|
|
71
|
+
try {
|
|
72
|
+
mkdirSync(sessionsDir, { recursive: true });
|
|
73
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
74
|
+
const outcomes = boundTurnOutcomes(turns);
|
|
75
|
+
writeFileSync(tmp, JSON.stringify({ version: VERSION, sessionId, folds: Object.fromEntries(boundFoldState(state)), ...(outcomes.length > 0 ? { turns: outcomes } : {}) }));
|
|
76
|
+
renameSync(tmp, path);
|
|
77
|
+
} catch {
|
|
78
|
+
// best effort; the view falls back to defaults on the next resume
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Read the session's fold state; an absent or unreadable sidecar reads as empty. */
|
|
83
|
+
export function loadWorkTreeFolds(sessionsDir: string, sessionId: string): Array<[string, boolean]> {
|
|
84
|
+
const path = sidecarPath(sessionsDir, sessionId);
|
|
85
|
+
if (!path || !existsSync(path)) return [];
|
|
86
|
+
try {
|
|
87
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
88
|
+
if (!parsed || typeof parsed !== 'object') return [];
|
|
89
|
+
const folds = (parsed as { folds?: unknown }).folds;
|
|
90
|
+
if (!folds || typeof folds !== 'object' || Array.isArray(folds)) return [];
|
|
91
|
+
return boundFoldState(Object.entries(folds as Record<string, unknown>));
|
|
92
|
+
} catch {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Read the session's saved failed/cancelled turn endings; an absent or unreadable sidecar reads as none. */
|
|
98
|
+
export function loadWorkTreeTurnOutcomes(sessionsDir: string, sessionId: string): TurnOutcomeRecord[] {
|
|
99
|
+
const path = sidecarPath(sessionsDir, sessionId);
|
|
100
|
+
if (!path || !existsSync(path)) return [];
|
|
101
|
+
try {
|
|
102
|
+
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'));
|
|
103
|
+
if (!parsed || typeof parsed !== 'object') return [];
|
|
104
|
+
const turns = (parsed as { turns?: unknown }).turns;
|
|
105
|
+
return Array.isArray(turns) ? boundTurnOutcomes(turns) : [];
|
|
106
|
+
} catch {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove fold sidecars whose session file no longer exists, and staging files
|
|
113
|
+
* a crash left behind. Returns how many files were removed.
|
|
114
|
+
*/
|
|
115
|
+
export function sweepOrphanWorkTreeFolds(sessionsDir: string, now = Date.now()): number {
|
|
116
|
+
if (!sessionsDir || !existsSync(sessionsDir)) return 0;
|
|
117
|
+
let removed = 0;
|
|
118
|
+
let names: string[];
|
|
119
|
+
try {
|
|
120
|
+
names = readdirSync(sessionsDir);
|
|
121
|
+
} catch {
|
|
122
|
+
return 0;
|
|
123
|
+
}
|
|
124
|
+
const present = new Set(names);
|
|
125
|
+
for (const name of names) {
|
|
126
|
+
try {
|
|
127
|
+
if (name.includes(`${SUFFIX}.tmp-`)) {
|
|
128
|
+
if (now - statSync(join(sessionsDir, name)).mtimeMs > TMP_MAX_AGE_MS) { unlinkSync(join(sessionsDir, name)); removed++; }
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (!name.endsWith(SUFFIX)) continue;
|
|
132
|
+
const stem = name.slice(0, -SUFFIX.length);
|
|
133
|
+
// Settled for an hour first: a session folds things before its first save writes the session file.
|
|
134
|
+
if (!present.has(`${stem}.jsonl`) && now - statSync(join(sessionsDir, name)).mtimeMs > TMP_MAX_AGE_MS) {
|
|
135
|
+
unlinkSync(join(sessionsDir, name));
|
|
136
|
+
removed++;
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// a file that vanished or cannot be removed is left for the next sweep
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return removed;
|
|
143
|
+
}
|