@deepseek-harness-tui/dsh-tui 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -256
- package/bin/dsh-tui.js +172 -5
- package/cordis.patch.yml +16 -4
- package/dsh-ecosystem-spec/protocols/profile-definitions.d.ts +13 -13
- package/dsh-ecosystem-spec/protocols/profile-definitions.js +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel-http.d.ts +15 -15
- package/dsh-ecosystem-spec/protocols/tui-channel-http.js +32 -32
- package/dsh-ecosystem-spec/protocols/tui-channel.d.ts +59 -59
- package/dsh-ecosystem-spec/protocols/tui-channel.js +166 -166
- package/dsh-ecosystem-spec/protocols/tui-contributions.d.ts +42 -42
- package/dsh-ecosystem-spec/protocols/tui-contributions.js +143 -143
- package/dsh-ecosystem-spec/registry/README.md +14 -14
- package/dsh-ecosystem-spec/registry/host-descriptor.tui.example.json +56 -56
- package/dsh-ecosystem-spec/registry/permissions-0.1.json +13 -13
- package/dsh-ecosystem-spec/registry/registry-0.15.json +149 -149
- package/lib/types/cc/hyperlink.d.ts +7 -1
- package/lib/types/cc/hyperlink.d.ts.map +1 -1
- package/lib/types/cc/hyperlink.js +3 -2
- package/lib/types/cc/markdown.d.ts.map +1 -1
- package/lib/types/cc/markdown.js +33 -3
- package/lib/types/cc/sessionColors.d.ts +27 -0
- package/lib/types/cc/sessionColors.d.ts.map +1 -0
- package/lib/types/cc/sessionColors.js +39 -0
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +6 -0
- package/lib/types/components/AutoRecapRow.d.ts +23 -0
- package/lib/types/components/AutoRecapRow.d.ts.map +1 -0
- package/lib/types/components/AutoRecapRow.js +23 -0
- package/lib/types/components/ColorPicker.d.ts +17 -0
- package/lib/types/components/ColorPicker.d.ts.map +1 -0
- package/lib/types/components/ColorPicker.js +20 -0
- package/lib/types/components/CommandSuggestions.d.ts +3 -1
- package/lib/types/components/CommandSuggestions.d.ts.map +1 -1
- package/lib/types/components/ContextBarView.d.ts +33 -0
- package/lib/types/components/ContextBarView.d.ts.map +1 -0
- package/lib/types/components/ContextBarView.js +46 -0
- package/lib/types/components/EffortInputBorder.d.ts +12 -1
- package/lib/types/components/EffortInputBorder.d.ts.map +1 -1
- package/lib/types/components/EffortInputBorder.js +22 -5
- package/lib/types/components/FileActionsPanel.d.ts +22 -0
- package/lib/types/components/FileActionsPanel.d.ts.map +1 -0
- package/lib/types/components/FileActionsPanel.js +28 -0
- package/lib/types/components/FileSuggestions.d.ts +3 -1
- package/lib/types/components/FileSuggestions.d.ts.map +1 -1
- package/lib/types/components/MessageList.d.ts +3 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +13 -6
- package/lib/types/components/ModelPicker.d.ts +26 -4
- package/lib/types/components/ModelPicker.d.ts.map +1 -1
- package/lib/types/components/ModelPicker.js +26 -6
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +365 -57
- package/lib/types/components/RecapPanel.d.ts +25 -0
- package/lib/types/components/RecapPanel.d.ts.map +1 -0
- package/lib/types/components/RecapPanel.js +55 -0
- package/lib/types/components/ScrollbarGutter.d.ts.map +1 -1
- package/lib/types/components/ScrollbarGutter.js +55 -4
- package/lib/types/components/SubagentCard.d.ts.map +1 -1
- package/lib/types/components/SubagentCard.js +6 -4
- package/lib/types/components/SuggestionCard.d.ts +4 -2
- package/lib/types/components/SuggestionCard.d.ts.map +1 -1
- package/lib/types/components/TimelineRail.d.ts.map +1 -1
- package/lib/types/components/TimelineRail.js +9 -2
- package/lib/types/components/design-system/ThemeProvider.d.ts +7 -0
- package/lib/types/components/design-system/ThemeProvider.d.ts.map +1 -1
- package/lib/types/components/design-system/ThemeProvider.js +2 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +8 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +36 -8
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +7 -3
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +41 -9
- package/lib/types/dsh-adapter/channel.d.ts +101 -2
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +969 -93
- package/lib/types/dsh-adapter/compat/index.d.ts +8 -6
- package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/index.js +8 -6
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts +150 -0
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/sessionLog.js +518 -41
- package/lib/types/dsh-adapter/contract.d.ts +1 -1
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +1 -0
- package/lib/types/dsh-adapter/extension-events.d.ts +1 -1
- package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.d.ts +10 -0
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +5 -0
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +317 -8
- package/lib/types/dsh-adapter/presets.d.ts +18 -0
- package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
- package/lib/types/dsh-adapter/presets.js +31 -0
- package/lib/types/dsh-adapter/providerWizard.d.ts +29 -1
- package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
- package/lib/types/dsh-adapter/providerWizard.js +89 -5
- package/lib/types/dsh-adapter/questions.d.ts +19 -2
- package/lib/types/dsh-adapter/questions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/questions.js +53 -10
- package/lib/types/dsh-adapter/recap.d.ts +49 -0
- package/lib/types/dsh-adapter/recap.d.ts.map +1 -0
- package/lib/types/dsh-adapter/recap.js +121 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts +314 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sessionTree.js +954 -0
- package/lib/types/dsh-adapter/shortcuts.d.ts +15 -44
- package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -1
- package/lib/types/dsh-adapter/shortcuts.js +28 -165
- package/lib/types/dsh-adapter/types.d.ts +1 -1
- package/lib/types/dsh-adapter/types.d.ts.map +1 -1
- package/lib/types/herdr.js +1 -1
- package/lib/types/i18n.d.ts +604 -176
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +206 -72
- package/lib/types/ink/log-update.d.ts.map +1 -1
- package/lib/types/ink/log-update.js +21 -0
- package/lib/types/migrationPrefs.d.ts +60 -0
- package/lib/types/migrationPrefs.d.ts.map +1 -0
- package/lib/types/migrationPrefs.js +97 -0
- package/lib/types/modelGroups.d.ts +72 -0
- package/lib/types/modelGroups.d.ts.map +1 -0
- package/lib/types/modelGroups.js +97 -0
- package/lib/types/modelRecents.d.ts +39 -0
- package/lib/types/modelRecents.d.ts.map +1 -0
- package/lib/types/modelRecents.js +97 -0
- package/lib/types/native-ts/yoga-layout/index.d.ts +11 -0
- package/lib/types/native-ts/yoga-layout/index.d.ts.map +1 -1
- package/lib/types/native-ts/yoga-layout/index.js +31 -1
- package/lib/types/oauth.d.ts +15 -0
- package/lib/types/oauth.d.ts.map +1 -0
- package/lib/types/oauth.js +14 -0
- package/lib/types/plugin-spec/registry.d.ts +1 -1
- package/lib/types/plugin-spec/registry.js +1 -1
- package/lib/types/reload.d.ts +109 -0
- package/lib/types/reload.d.ts.map +1 -0
- package/lib/types/reload.js +118 -0
- package/lib/types/screens/Chat.d.ts +3 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +588 -64
- package/lib/types/screens/SessionTree.d.ts +27 -0
- package/lib/types/screens/SessionTree.d.ts.map +1 -0
- package/lib/types/screens/SessionTree.js +555 -0
- package/lib/types/screens/StatusLine.d.ts +0 -9
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +215 -52
- package/lib/types/screens/StatusMetrics.d.ts +35 -3
- package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
- package/lib/types/screens/StatusMetrics.js +63 -8
- package/lib/types/screens/chatOverlay.d.ts +17 -1
- package/lib/types/screens/chatOverlay.d.ts.map +1 -1
- package/lib/types/screens/chatOverlay.js +3 -1
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +22 -4
- package/lib/types/update.d.ts +159 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +496 -30
- package/lib/types/utils/debug.d.ts +1 -1
- package/lib/types/utils/debug.js +3 -3
- package/lib/types/utils/fileTarget.d.ts +46 -0
- package/lib/types/utils/fileTarget.d.ts.map +1 -0
- package/lib/types/utils/fileTarget.js +178 -0
- package/lib/types/utils/keymap.d.ts +134 -0
- package/lib/types/utils/keymap.d.ts.map +1 -0
- package/lib/types/utils/keymap.js +311 -0
- package/lib/types/utils/mentions.d.ts +25 -2
- package/lib/types/utils/mentions.d.ts.map +1 -1
- package/lib/types/utils/mentions.js +69 -10
- package/lib/types/utils/openExternal.d.ts +50 -0
- package/lib/types/utils/openExternal.d.ts.map +1 -0
- package/lib/types/utils/openExternal.js +170 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/LICENSE +21 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +140 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/cordis.patch.yml +11 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/dsh-plugin.json +44 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.d.ts +14 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/command.js +67 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.d.ts +64 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/credentials.js +170 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.d.ts +79 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/index.js +196 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.d.ts +68 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.js +193 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.d.ts +32 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/opener.js +101 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.d.ts +31 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/profiles.js +68 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.d.ts +69 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/lib/service.js +129 -0
- package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +70 -0
- package/node_modules/@dsh-std/command/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/command/LICENSE +21 -21
- package/node_modules/@dsh-std/command/README.md +26 -26
- package/node_modules/@dsh-std/command/README.zh.md +28 -28
- package/node_modules/@dsh-std/command/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/command/package.json +22 -22
- package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/node_modules/@dsh-std/connection/README.md +42 -42
- package/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/node_modules/@dsh-std/connection/package.json +20 -20
- package/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/core/LICENSE +21 -21
- package/node_modules/@dsh-std/core/README.md +13 -13
- package/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/core/package.json +21 -21
- package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/node_modules/@dsh-std/manifest/README.md +5 -5
- package/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/manifest/package.json +20 -20
- package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/messages/LICENSE +21 -21
- package/node_modules/@dsh-std/messages/README.md +5 -5
- package/node_modules/@dsh-std/messages/README.zh.md +7 -7
- package/node_modules/@dsh-std/messages/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/messages/package.json +20 -20
- package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -54
- package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/node_modules/@dsh-std/presentation/README.md +27 -27
- package/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/presentation/package.json +25 -25
- package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -8
- package/node_modules/@dsh-std/storage/LICENSE +21 -21
- package/node_modules/@dsh-std/storage/README.md +5 -5
- package/node_modules/@dsh-std/storage/README.zh.md +7 -7
- package/node_modules/@dsh-std/storage/lib/index.js.map +1 -1
- package/node_modules/@dsh-std/storage/package.json +20 -20
- package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -56
- package/package.json +17 -5
- package/presets/liangshen/.dsh-tui-managed.json +1 -1
- package/presets/liangshen/instruction-hint.mjs +8 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -5
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -20
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -144
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -9
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -21
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +27 -27
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +29 -29
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +25 -25
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -42
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -20
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -8
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -21
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -13
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -1
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -21
|
@@ -0,0 +1,954 @@
|
|
|
1
|
+
export const TREE_FILTERS = ['default', 'no-tools', 'user-only', 'all'];
|
|
2
|
+
/**
|
|
3
|
+
* Confirm-time preview for a USER-message pick: how much of its branch the
|
|
4
|
+
* drop removes. Undefined for non-user entries (they keep through their
|
|
5
|
+
* step) and whenever the turn cannot be located in the loaded events.
|
|
6
|
+
*/
|
|
7
|
+
export function droppedTurnInfo(data, entry) {
|
|
8
|
+
if (entry.kind !== 'user')
|
|
9
|
+
return undefined;
|
|
10
|
+
const facts = data.rewindFacts.get(entry.sessionId);
|
|
11
|
+
if (facts === undefined)
|
|
12
|
+
return undefined;
|
|
13
|
+
const turn = facts.turns.find(range => entry.seq > range.start && entry.seq <= range.end);
|
|
14
|
+
if (turn === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
return {
|
|
17
|
+
droppedEntries: turn.entries,
|
|
18
|
+
coversBranch: facts.tailComplete && facts.ownEntries > 0 && turn.entries === facts.ownEntries,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Preview cap for one entry line (the screen truncates further to width). */
|
|
22
|
+
const ENTRY_PREVIEW_LIMIT = 120;
|
|
23
|
+
/** Tool arguments preview cap inside a tool entry line. */
|
|
24
|
+
const TOOL_ARGS_PREVIEW_LIMIT = 60;
|
|
25
|
+
/** Whitespace-folded, capped one-line preview. */
|
|
26
|
+
function preview(text, limit = ENTRY_PREVIEW_LIMIT) {
|
|
27
|
+
const flat = text.replace(/\s+/g, ' ').trim();
|
|
28
|
+
return flat.length <= limit ? flat : `${flat.slice(0, limit)}…`;
|
|
29
|
+
}
|
|
30
|
+
/** All text blocks joined (assistant messages, compact summaries). */
|
|
31
|
+
function textOf(content) {
|
|
32
|
+
return (content ?? []).map(block => (block.type === 'text' ? (block.text ?? '') : '')).join('').trim();
|
|
33
|
+
}
|
|
34
|
+
/** FIRST text block only — `@`-mention attachments ride as later blocks
|
|
35
|
+
* (channel.ts firstTextOf, issue #15). */
|
|
36
|
+
function firstTextOf(content) {
|
|
37
|
+
return (content ?? []).find(block => block.type === 'text')?.text?.trim() ?? '';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Coalesce runs of same-type assistant/chunk deltas into single synthetic
|
|
41
|
+
* events for REPLAY only. A streamed turn logs one event per token (~100k
|
|
42
|
+
* events in long sessions); replaying them one at a time costs per-chunk
|
|
43
|
+
* string growth on every row (quadratic in the turn's length). Merging is
|
|
44
|
+
* outcome-identical: ensureStreaming/ensureReasoning only read chunk.type
|
|
45
|
+
* and the concatenated text, and the row's seq comes from the run's FIRST
|
|
46
|
+
* chunk (the fork boundary rewindToNode derives from it). Parts join once —
|
|
47
|
+
* no quadratic concat. Live events never go through this.
|
|
48
|
+
*
|
|
49
|
+
* (Moved from channel.ts: the transcript replay and the tree extraction
|
|
50
|
+
* share it.)
|
|
51
|
+
*/
|
|
52
|
+
export function coalesceReplayEvents(events) {
|
|
53
|
+
const out = [];
|
|
54
|
+
let run = null;
|
|
55
|
+
const flush = () => {
|
|
56
|
+
if (run === null)
|
|
57
|
+
return;
|
|
58
|
+
const chunk = run.event.data.chunk;
|
|
59
|
+
out.push({
|
|
60
|
+
...run.event,
|
|
61
|
+
data: { ...run.event.data, chunk: { ...chunk, text: run.parts.join('') } },
|
|
62
|
+
});
|
|
63
|
+
run = null;
|
|
64
|
+
};
|
|
65
|
+
for (const event of events) {
|
|
66
|
+
if (event.type === 'assistant/chunk' &&
|
|
67
|
+
(event.data.chunk.type === 'text-delta' || event.data.chunk.type === 'reasoning-delta')) {
|
|
68
|
+
if (run !== null && run.type === event.data.chunk.type) {
|
|
69
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- durable replay data may lack text
|
|
70
|
+
run.parts.push(event.data.chunk.text ?? '');
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
flush();
|
|
74
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- durable replay data may lack text
|
|
75
|
+
run = { event, type: event.data.chunk.type, parts: [event.data.chunk.text ?? ''] };
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
flush();
|
|
79
|
+
out.push(event);
|
|
80
|
+
}
|
|
81
|
+
flush();
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Fork boundary for rewinding to the entry at `seq`: the seq just before its
|
|
86
|
+
* enclosing turn/start (DSH logs `turn/start → user/message → … → turn/end`,
|
|
87
|
+
* so a message's own seq sits inside an open turn and fork would reject it).
|
|
88
|
+
* This is the DROP-THE-TURN boundary — used for user messages (the turn is
|
|
89
|
+
* dropped, its prompt returns to the input) and as the fallback for entries
|
|
90
|
+
* of a still-open turn. Falls back to `seq` itself when no enclosing
|
|
91
|
+
* turn/start exists (bookkeeping between turns).
|
|
92
|
+
*/
|
|
93
|
+
export function turnBoundary(events, seq) {
|
|
94
|
+
const start = events[seq]?.type === 'turn/end' ? seq - 1 : seq;
|
|
95
|
+
for (let i = start; i >= 0; i--) {
|
|
96
|
+
const event = events[i];
|
|
97
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: seq may exceed events
|
|
98
|
+
if (event === undefined)
|
|
99
|
+
break;
|
|
100
|
+
if (event.type === 'turn/start')
|
|
101
|
+
return event.seq - 1;
|
|
102
|
+
if (event.type === 'turn/end')
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
return seq;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Rewind target for a tree entry — pi's navigateTree semantics mapped onto
|
|
109
|
+
* DSH's turn-closed fork constraint (the seed must not end inside an open
|
|
110
|
+
* turn). Callers must pass the FULL session log (seq === array index).
|
|
111
|
+
*
|
|
112
|
+
* - user message → DROP its turn: the seq just before the enclosing
|
|
113
|
+
* turn/start (turnBoundary); the turn's prompt returns to the input for
|
|
114
|
+
* re-editing. -1 when that turn is the log's own first turn.
|
|
115
|
+
* - any other entry inside a CLOSED turn (assistant/tool/notice/interrupt)
|
|
116
|
+
* → KEEP through the entry's enclosing STEP: the boundary is that step's
|
|
117
|
+
* step/end, with closeTurn set. DSH agentic turns are marathons (one
|
|
118
|
+
* prompt → N steps → thousands of events), so turn-granular keeping is
|
|
119
|
+
* useless mid-turn — picking a bash call of step 2 in a 6-step turn must
|
|
120
|
+
* not retain steps 3-6. The step is the finest SAFE cut: a step closes
|
|
121
|
+
* with every tool call answered, so the replayed context never dangles a
|
|
122
|
+
* tool_call the API would reject. When no step/end follows the entry
|
|
123
|
+
* inside its turn (no-step turns, or the entry sits past the last step),
|
|
124
|
+
* the boundary is the turn/end as before (no closeTurn).
|
|
125
|
+
* - entry between turns (a compact checkpoint) → its own seq.
|
|
126
|
+
* - entry inside the still-OPEN last turn → a closed step ahead of it is
|
|
127
|
+
* still a valid mid-turn cut (closeTurn set; the open tail drops); with no
|
|
128
|
+
* step/end ahead the turn cannot be kept at all, so it falls back to
|
|
129
|
+
* dropping the turn (turnBoundary) and restoring its prompt.
|
|
130
|
+
*/
|
|
131
|
+
export function rewindTarget(events, seq) {
|
|
132
|
+
const selected = events[seq];
|
|
133
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: seq may exceed events
|
|
134
|
+
if (selected === undefined || selected.type === 'user/message') {
|
|
135
|
+
return { boundary: turnBoundary(events, seq) };
|
|
136
|
+
}
|
|
137
|
+
if (selected.type === 'turn/end')
|
|
138
|
+
return { boundary: seq }; // interrupt/notice entries keep through their turn's end
|
|
139
|
+
// The enclosing turn's number — needed to close a mid-turn cut.
|
|
140
|
+
let turn;
|
|
141
|
+
for (let i = seq; i >= 0; i--) {
|
|
142
|
+
const event = events[i];
|
|
143
|
+
if (event.type === 'turn/start') {
|
|
144
|
+
turn = event.data.turn;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
if (event.type === 'turn/end')
|
|
148
|
+
break; // between turns: no enclosing turn
|
|
149
|
+
}
|
|
150
|
+
for (let i = seq + 1; i < events.length; i++) {
|
|
151
|
+
const event = events[i];
|
|
152
|
+
if (event.type === 'step/end' && turn !== undefined) {
|
|
153
|
+
return { boundary: event.seq, closeTurn: turn };
|
|
154
|
+
}
|
|
155
|
+
if (event.type === 'turn/end')
|
|
156
|
+
return { boundary: event.seq };
|
|
157
|
+
if (event.type === 'turn/start')
|
|
158
|
+
break; // between turns: keep through the entry
|
|
159
|
+
}
|
|
160
|
+
return { boundary: turnBoundary(events, seq) };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Fork target for a tree entry — pi's `/fork` semantics: the picked entry is
|
|
164
|
+
* KEPT (a fork branches the conversation; it does not re-edit the past).
|
|
165
|
+
*
|
|
166
|
+
* - user message → keep THROUGH the message itself: the boundary is the
|
|
167
|
+
* entry's own seq and the turn's reply drops. The cut ends inside the
|
|
168
|
+
* still-open turn, so closeTurn carries the enclosing turn's number for
|
|
169
|
+
* the synthetic turn/end (the same shape a real interrupt writes). A
|
|
170
|
+
* between-turns plugin message (a compact checkpoint) is already
|
|
171
|
+
* turn-closed — no closer.
|
|
172
|
+
* - any other entry → identical to rewindTarget (keep through the
|
|
173
|
+
* entry's enclosing step / turn end).
|
|
174
|
+
*
|
|
175
|
+
* Unlike a rewind pick, a turn-0 user message forks fine (boundary = its own
|
|
176
|
+
* seq, never -1), and nothing returns to the input for re-editing.
|
|
177
|
+
*/
|
|
178
|
+
export function forkTarget(events, seq) {
|
|
179
|
+
const selected = events[seq];
|
|
180
|
+
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: seq may exceed events
|
|
181
|
+
if (selected === undefined || selected.type !== 'user/message') {
|
|
182
|
+
return rewindTarget(events, seq);
|
|
183
|
+
}
|
|
184
|
+
// The enclosing turn's number — needed to close the mid-turn cut. A
|
|
185
|
+
// turn/end encountered first means the entry sits between turns.
|
|
186
|
+
let turn;
|
|
187
|
+
for (let i = seq; i >= 0; i--) {
|
|
188
|
+
const event = events[i];
|
|
189
|
+
if (event.type === 'turn/start') {
|
|
190
|
+
turn = event.data.turn;
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
if (event.type === 'turn/end')
|
|
194
|
+
break; // between turns: already closed
|
|
195
|
+
}
|
|
196
|
+
return turn === undefined ? { boundary: seq } : { boundary: seq, closeTurn: turn };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Tail window of an over-budget LIVE session's events, aligned to whole
|
|
200
|
+
* turns. Recent turns are the likely rewind targets, so the budget keeps
|
|
201
|
+
* the tail — but a slice can open mid-turn, and entries of a partial turn
|
|
202
|
+
* make broken rows: when that partial turn is the log's OWN first turn its
|
|
203
|
+
* rewind boundary is -1 ("cannot rewind to the very first message"), yet
|
|
204
|
+
* the slice hides the turn/start@0 that firstTurn marking keys on — the
|
|
205
|
+
* row would confirm and then fail inside rewindToNode. So: align to the
|
|
206
|
+
* first whole turn inside the window. When the window holds no turn/start
|
|
207
|
+
* at all, the log's LAST turn alone overflows the budget — retry over the
|
|
208
|
+
* log before that turn: its earlier complete turns are perfectly good
|
|
209
|
+
* rewind targets and must not vanish behind one giant turn. Only when the
|
|
210
|
+
* oversized turn IS the log's own first turn does the window drop to empty
|
|
211
|
+
* — no complete turn in view means no safe rewind target to show. Entries
|
|
212
|
+
* dropped here stay rewindable through the real session; the slice only
|
|
213
|
+
* narrows what the tree displays.
|
|
214
|
+
*/
|
|
215
|
+
export function liveTailWindow(events, remaining) {
|
|
216
|
+
if (events.length <= remaining)
|
|
217
|
+
return events;
|
|
218
|
+
if (remaining <= 0)
|
|
219
|
+
return [];
|
|
220
|
+
let scope = events;
|
|
221
|
+
for (;;) {
|
|
222
|
+
const sliced = scope.slice(scope.length - remaining);
|
|
223
|
+
const firstStart = sliced.findIndex(event => event.type === 'turn/start');
|
|
224
|
+
if (firstStart === 0)
|
|
225
|
+
return sliced;
|
|
226
|
+
if (firstStart > 0)
|
|
227
|
+
return sliced.slice(firstStart);
|
|
228
|
+
// No turn/start in the window: the oversized turn spans it whole. Cut
|
|
229
|
+
// the log just before that turn and retry — the window over the shorter
|
|
230
|
+
// scope either fits complete earlier turns or overflows again into an
|
|
231
|
+
// even earlier oversized turn (the loop converges: each cut removes at
|
|
232
|
+
// least one turn).
|
|
233
|
+
let lastStart = -1;
|
|
234
|
+
for (let i = scope.length - 1; i >= 0; i--) {
|
|
235
|
+
if (scope[i].type === 'turn/start') {
|
|
236
|
+
lastStart = i;
|
|
237
|
+
break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (lastStart <= 0)
|
|
241
|
+
return [];
|
|
242
|
+
scope = scope.slice(0, lastStart);
|
|
243
|
+
if (scope.length <= remaining)
|
|
244
|
+
return scope;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** The turn's first human-typed text (the prompt rewind restores for editing). */
|
|
248
|
+
export function turnUserText(events, seq) {
|
|
249
|
+
const boundary = rewindTarget(events, seq).boundary;
|
|
250
|
+
// A boundary at/after the entry means its turn is KEPT (assistant/tool
|
|
251
|
+
// targets, between-turns checkpoints) — pi restores editor text only for
|
|
252
|
+
// dropped user messages, and scanning on from here would cross the NEXT
|
|
253
|
+
// turn/start and steal that turn's prompt.
|
|
254
|
+
if (boundary >= seq)
|
|
255
|
+
return '';
|
|
256
|
+
for (let i = boundary + 1; i < events.length; i++) {
|
|
257
|
+
const event = events[i];
|
|
258
|
+
if (event.type === 'turn/end')
|
|
259
|
+
break;
|
|
260
|
+
if (event.type !== 'user/message')
|
|
261
|
+
continue;
|
|
262
|
+
if (event.data.source.kind !== 'user')
|
|
263
|
+
continue;
|
|
264
|
+
const text = firstTextOf(event.data.content);
|
|
265
|
+
if (text)
|
|
266
|
+
return text;
|
|
267
|
+
}
|
|
268
|
+
return '';
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* One session's displayable entries, in seq order. Mirrors renderEvent's
|
|
272
|
+
* choices (channel.ts): human user messages only, compaction checkpoints as
|
|
273
|
+
* compact rows, assistant text (chunk-only text from aborted turns kept with
|
|
274
|
+
* an `aborted` label), tool cards minus ask_user_question, interrupts and
|
|
275
|
+
* failed turns. Bookkeeping events (turn/step markers, titles, requests,
|
|
276
|
+
* `session/end-seed`, activity frames…) never become entries.
|
|
277
|
+
*/
|
|
278
|
+
export function extractEntries(sessionId, events) {
|
|
279
|
+
const entries = [];
|
|
280
|
+
// First-turn marking applies only when the log still starts at seq 0 — the
|
|
281
|
+
// complete history, whose turn 0 can never rewind. A budget-truncated tail
|
|
282
|
+
// opens mid-log; its first visible turn has a valid boundary in the full
|
|
283
|
+
// log (rewindToNode computes it there), so it must stay selectable.
|
|
284
|
+
const markFirstTurn = events.find(event => event.type === 'turn/start')?.seq === 0;
|
|
285
|
+
let turnsSeen = 0;
|
|
286
|
+
let inFirstTurn = false;
|
|
287
|
+
const push = (entry) => {
|
|
288
|
+
entries.push({ ...entry, sessionId, ...(inFirstTurn ? { firstTurn: true } : {}) });
|
|
289
|
+
return entries.length - 1;
|
|
290
|
+
};
|
|
291
|
+
/** Steps with a settled assistant/message (chunk-run tentatives drop). */
|
|
292
|
+
const seenSteps = new Set();
|
|
293
|
+
/** stepKey → entry indices of chunk-synthesized assistant texts. One step
|
|
294
|
+
* can yield SEVERAL runs when a non-chunk event interleaves the deltas
|
|
295
|
+
* (coalescing flushes at the boundary) — track them all, or earlier runs
|
|
296
|
+
* survive as ghost duplicates of the settled message. */
|
|
297
|
+
const tentatives = new Map();
|
|
298
|
+
/** callId → entry index of an unsettled tool card. */
|
|
299
|
+
const openTools = new Map();
|
|
300
|
+
/** Turns whose turn/end said aborted/interrupted. */
|
|
301
|
+
const abortedTurns = new Set();
|
|
302
|
+
for (const event of coalesceReplayEvents(events)) {
|
|
303
|
+
if (event.type === 'turn/start') {
|
|
304
|
+
turnsSeen += 1;
|
|
305
|
+
inFirstTurn = markFirstTurn && turnsSeen === 1;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
switch (event.type) {
|
|
309
|
+
case 'user/message': {
|
|
310
|
+
const source = event.data.source;
|
|
311
|
+
if (source.kind === 'plugin' && source.plugin === 'compact') {
|
|
312
|
+
const summary = textOf(event.data.content);
|
|
313
|
+
push({
|
|
314
|
+
seq: event.seq,
|
|
315
|
+
kind: 'compact',
|
|
316
|
+
text: preview(summary || '(compaction)'),
|
|
317
|
+
searchText: `compact ${summary}`,
|
|
318
|
+
time: event.time,
|
|
319
|
+
});
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
if (source.kind !== 'user')
|
|
323
|
+
break;
|
|
324
|
+
const text = firstTextOf(event.data.content);
|
|
325
|
+
if (text) {
|
|
326
|
+
push({
|
|
327
|
+
seq: event.seq,
|
|
328
|
+
kind: 'user',
|
|
329
|
+
text: preview(text),
|
|
330
|
+
searchText: `user ${text}`,
|
|
331
|
+
time: event.time,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
case 'assistant/message': {
|
|
337
|
+
const { turn, step } = event.data;
|
|
338
|
+
seenSteps.add(`${turn}:${step}`);
|
|
339
|
+
const text = textOf(event.data.message.content);
|
|
340
|
+
// pi hides assistant messages with only tool calls (no text).
|
|
341
|
+
if (text) {
|
|
342
|
+
push({
|
|
343
|
+
seq: event.seq,
|
|
344
|
+
kind: 'assistant',
|
|
345
|
+
text: preview(text),
|
|
346
|
+
searchText: `assistant ${text}`,
|
|
347
|
+
time: event.time,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
case 'assistant/chunk': {
|
|
353
|
+
const chunk = event.data.chunk;
|
|
354
|
+
if (chunk.type !== 'text-delta')
|
|
355
|
+
break;
|
|
356
|
+
const text = 'text' in chunk ? (chunk.text ?? '') : '';
|
|
357
|
+
if (!text.trim())
|
|
358
|
+
break;
|
|
359
|
+
const key = `${event.data.turn}:${event.data.step}`;
|
|
360
|
+
const index = push({
|
|
361
|
+
seq: event.seq,
|
|
362
|
+
kind: 'assistant',
|
|
363
|
+
text: preview(text),
|
|
364
|
+
searchText: `assistant ${text}`,
|
|
365
|
+
time: event.time,
|
|
366
|
+
});
|
|
367
|
+
const group = tentatives.get(key);
|
|
368
|
+
if (group === undefined)
|
|
369
|
+
tentatives.set(key, [index]);
|
|
370
|
+
else
|
|
371
|
+
group.push(index);
|
|
372
|
+
break;
|
|
373
|
+
}
|
|
374
|
+
case 'tool/call': {
|
|
375
|
+
if (event.data.name === 'ask_user_question')
|
|
376
|
+
break;
|
|
377
|
+
const args = preview(event.data.arguments, TOOL_ARGS_PREVIEW_LIMIT);
|
|
378
|
+
const index = push({
|
|
379
|
+
seq: event.seq,
|
|
380
|
+
kind: 'tool',
|
|
381
|
+
text: `[${event.data.name}] ${args}`,
|
|
382
|
+
searchText: `tool ${event.data.name} ${event.data.arguments}`,
|
|
383
|
+
time: event.time,
|
|
384
|
+
toolStatus: 'running',
|
|
385
|
+
});
|
|
386
|
+
openTools.set(event.data.callId, index);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
case 'tool/result': {
|
|
390
|
+
const index = openTools.get(event.data.message.source.callId);
|
|
391
|
+
if (index === undefined)
|
|
392
|
+
break;
|
|
393
|
+
const entry = entries[index];
|
|
394
|
+
entries[index] = {
|
|
395
|
+
...entry,
|
|
396
|
+
toolStatus: event.data.error === undefined ? 'ok' : 'error',
|
|
397
|
+
};
|
|
398
|
+
openTools.delete(event.data.message.source.callId);
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
case 'turn/end': {
|
|
402
|
+
const reason = event.data.reason;
|
|
403
|
+
if (reason.kind === 'aborted' || reason.kind === 'interrupted') {
|
|
404
|
+
abortedTurns.add(event.data.turn);
|
|
405
|
+
push({ seq: event.seq, kind: 'interrupt', text: 'interrupted', searchText: 'interrupt interrupted', time: event.time });
|
|
406
|
+
}
|
|
407
|
+
else if (reason.kind !== 'completed') {
|
|
408
|
+
const detail = reason.kind === 'error' ? reason.error.message : '';
|
|
409
|
+
push({
|
|
410
|
+
seq: event.seq,
|
|
411
|
+
kind: 'notice',
|
|
412
|
+
text: preview(`turn ${reason.kind}${detail ? ` · ${detail}` : ''}`),
|
|
413
|
+
searchText: `notice turn ${reason.kind} ${detail}`,
|
|
414
|
+
time: event.time,
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
// The turn CLOSES here — including turn 0. Entries logged BETWEEN
|
|
418
|
+
// turns (a compact checkpoint) are not turn-0 entries: their rewind
|
|
419
|
+
// boundary is the entry itself, perfectly valid, so the flag must
|
|
420
|
+
// not leak past this point and get them refused as "first message".
|
|
421
|
+
inFirstTurn = false;
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
default:
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Chunk-synthesized assistant entries lose to a settled assistant/message
|
|
429
|
+
// for the same step; survivors of an aborted turn get the marker.
|
|
430
|
+
const drop = new Set();
|
|
431
|
+
for (const [key, indices] of tentatives) {
|
|
432
|
+
if (seenSteps.has(key)) {
|
|
433
|
+
for (const index of indices)
|
|
434
|
+
drop.add(index);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const turn = Number(key.slice(0, key.indexOf(':')));
|
|
438
|
+
if (abortedTurns.has(turn)) {
|
|
439
|
+
for (const index of indices) {
|
|
440
|
+
entries[index] = { ...entries[index], label: 'aborted' };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return drop.size === 0 ? entries : entries.filter((_, index) => !drop.has(index));
|
|
445
|
+
}
|
|
446
|
+
/** A session's title event, if it ever got one (manual /rename or auto). */
|
|
447
|
+
function titleOf(events) {
|
|
448
|
+
let title;
|
|
449
|
+
for (const event of events) {
|
|
450
|
+
if (event.type === 'session/title') {
|
|
451
|
+
const data = event.data;
|
|
452
|
+
if (typeof data.title === 'string' && data.title !== '')
|
|
453
|
+
title = data.title;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return title;
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Assemble the family tree. Roots are sessions with no parentSession (or a
|
|
460
|
+
* parent missing from the family — a deleted log; the orphan's self-contained
|
|
461
|
+
* prefix keeps its history whole). Fork chains attach at the last entry with
|
|
462
|
+
* seq <= seedLength-1, walking UP the ancestor chain when the direct parent
|
|
463
|
+
* no longer displays that entry (seed-prefix trimming — the fork point lives
|
|
464
|
+
* in the shared prefix an ancestor shows); the anchored session's own tail
|
|
465
|
+
* past the anchor stays the first (main) branch. Sessions with no own
|
|
466
|
+
* entries get one placeholder node so the fork structure stays visible.
|
|
467
|
+
*/
|
|
468
|
+
export function buildSessionTree(sessions, liveSessionId, truncated = false) {
|
|
469
|
+
const byId = new Map(sessions.map(session => [session.id, session]));
|
|
470
|
+
// Corrupt headers can close a parent LOOP (a self-reference, or A↔B):
|
|
471
|
+
// attached under each other with no topmost member, every session in the
|
|
472
|
+
// loop is skipped by root selection and the whole loop — live chain
|
|
473
|
+
// included — vanishes from the panel. Cut the edge that closes each loop
|
|
474
|
+
// before anything hangs off parent links; walking in input order keeps the
|
|
475
|
+
// surviving root deterministic, and every structural read below goes
|
|
476
|
+
// through parentOf so the cut is observed consistently.
|
|
477
|
+
const cutEdges = new Set();
|
|
478
|
+
const parentOf = (session) => cutEdges.has(session.id) ? undefined : session.parentSession;
|
|
479
|
+
for (const session of sessions) {
|
|
480
|
+
const seen = new Set([session.id]);
|
|
481
|
+
let cursor = session;
|
|
482
|
+
for (;;) {
|
|
483
|
+
const parentId = parentOf(cursor);
|
|
484
|
+
if (parentId === undefined)
|
|
485
|
+
break;
|
|
486
|
+
const parent = byId.get(parentId);
|
|
487
|
+
if (parent === undefined)
|
|
488
|
+
break;
|
|
489
|
+
if (seen.has(parent.id)) {
|
|
490
|
+
cutEdges.add(cursor.id);
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
seen.add(parent.id);
|
|
494
|
+
cursor = parent;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// Coverage bookkeeping: coveredThrough(S) = the highest K such that every
|
|
498
|
+
// seq in [0..K] is displayed by S's chain or an ancestor's. A fork's
|
|
499
|
+
// inherited seed prefix duplicates that range, so the fork's own chain
|
|
500
|
+
// trims it — but ONLY the covered part: an unreadable/unloaded or
|
|
501
|
+
// budget-truncated parent shows nothing (or less than the full prefix),
|
|
502
|
+
// and trimming the full seedLength would hide inherited history nobody
|
|
503
|
+
// displays (the child's log is self-contained). Transparent sessions
|
|
504
|
+
// (unreadable/unloaded/empty) forward their own ancestors' coverage, so a
|
|
505
|
+
// fork of a dead branch still dedups against the grandparent's chain.
|
|
506
|
+
const coveredThrough = new Map();
|
|
507
|
+
const covering = new Set();
|
|
508
|
+
const coverOf = (id) => {
|
|
509
|
+
const known = coveredThrough.get(id);
|
|
510
|
+
if (known !== undefined)
|
|
511
|
+
return known;
|
|
512
|
+
const session = byId.get(id);
|
|
513
|
+
if (session === undefined || covering.has(id))
|
|
514
|
+
return -1;
|
|
515
|
+
covering.add(id);
|
|
516
|
+
const parentId = parentOf(session);
|
|
517
|
+
const parentCover = parentId !== undefined && byId.has(parentId) ? coverOf(parentId) : -1;
|
|
518
|
+
covering.delete(id);
|
|
519
|
+
let cover;
|
|
520
|
+
if (session.unreadable === true || session.unloaded === true || session.events.length === 0) {
|
|
521
|
+
cover = parentCover;
|
|
522
|
+
}
|
|
523
|
+
else {
|
|
524
|
+
const firstSeq = session.events[0].seq;
|
|
525
|
+
const lastSeq = session.events[session.events.length - 1].seq;
|
|
526
|
+
// The events claim [0..lastSeq] only when they CONNECT to the parent's
|
|
527
|
+
// coverage (a full log starts at 0; a coverage-skipped read starts at
|
|
528
|
+
// the parent's cutoff). Otherwise the range is an island and coverage
|
|
529
|
+
// stays at the parent's — the events still display, they just cannot
|
|
530
|
+
// vouch for a contiguous prefix.
|
|
531
|
+
cover = firstSeq <= parentCover + 1 ? Math.max(parentCover, lastSeq) : parentCover;
|
|
532
|
+
}
|
|
533
|
+
coveredThrough.set(id, cover);
|
|
534
|
+
return cover;
|
|
535
|
+
};
|
|
536
|
+
// Per-session chains: extracted entries, with the inherited seed prefix
|
|
537
|
+
// trimmed by what the ancestor chain ACTUALLY covers (never past the seed
|
|
538
|
+
// prefix — events beyond it are this session's own).
|
|
539
|
+
const chains = new Map();
|
|
540
|
+
const metas = new Map();
|
|
541
|
+
const rewindFacts = new Map();
|
|
542
|
+
for (const session of sessions) {
|
|
543
|
+
metas.set(session.id, {
|
|
544
|
+
title: titleOf(session.events),
|
|
545
|
+
createdAt: session.createdAt,
|
|
546
|
+
live: session.live,
|
|
547
|
+
unreadable: session.unreadable === true,
|
|
548
|
+
unloaded: session.unloaded === true,
|
|
549
|
+
});
|
|
550
|
+
const parentId = parentOf(session);
|
|
551
|
+
const trimFrom = parentId !== undefined && byId.has(parentId)
|
|
552
|
+
? Math.min(session.seedLength ?? 0, coverOf(parentId) + 1)
|
|
553
|
+
: 0;
|
|
554
|
+
const own = session.unreadable === true ? [] : extractEntries(session.id, session.events).filter(entry => entry.seq >= trimFrom);
|
|
555
|
+
// Rewind UX facts: own turn ranges (drop-turn confirm warning) and the
|
|
556
|
+
// branch-adopt target. A coverage/budget head cut can open the loaded
|
|
557
|
+
// events MID-turn — a turn/end then has no matching start; that turn
|
|
558
|
+
// gets no range and droppedTurnInfo stays silent for its entries rather
|
|
559
|
+
// than blaming the wrong span. Open turns end at the loaded tail.
|
|
560
|
+
{
|
|
561
|
+
const mutableTurns = [];
|
|
562
|
+
const lastSeq = session.events.length > 0 ? session.events[session.events.length - 1].seq : -1;
|
|
563
|
+
let lastTurnEnd = -1;
|
|
564
|
+
for (const event of session.events) {
|
|
565
|
+
if (event.type === 'turn/start') {
|
|
566
|
+
mutableTurns.push({ start: event.seq, end: lastSeq, entries: 0, closed: false });
|
|
567
|
+
}
|
|
568
|
+
else if (event.type === 'turn/end') {
|
|
569
|
+
lastTurnEnd = event.seq;
|
|
570
|
+
const open = mutableTurns[mutableTurns.length - 1];
|
|
571
|
+
if (open !== undefined && !open.closed) {
|
|
572
|
+
open.end = event.seq;
|
|
573
|
+
open.closed = true;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
// Both lists are seq-ordered: two-pointer the own entries into turns.
|
|
578
|
+
let turnIndex = 0;
|
|
579
|
+
for (const entry of own) {
|
|
580
|
+
while (turnIndex < mutableTurns.length && entry.seq > mutableTurns[turnIndex].end)
|
|
581
|
+
turnIndex++;
|
|
582
|
+
const turn = mutableTurns[turnIndex];
|
|
583
|
+
if (turn !== undefined && entry.seq > turn.start)
|
|
584
|
+
turn.entries++;
|
|
585
|
+
}
|
|
586
|
+
rewindFacts.set(session.id, {
|
|
587
|
+
turns: mutableTurns,
|
|
588
|
+
ownEntries: own.length,
|
|
589
|
+
tailComplete: session.tailComplete === true,
|
|
590
|
+
...(session.tailComplete === true && lastTurnEnd >= 0 ? { tipBoundary: lastTurnEnd } : {}),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
if (own.length === 0) {
|
|
594
|
+
chains.set(session.id, [
|
|
595
|
+
{ id: `${session.id}:head`, entry: null, sessionId: session.id, branchHead: true, children: [] },
|
|
596
|
+
]);
|
|
597
|
+
continue;
|
|
598
|
+
}
|
|
599
|
+
chains.set(session.id, own.map((entry, index) => ({
|
|
600
|
+
id: `${session.id}:${entry.seq}`,
|
|
601
|
+
entry,
|
|
602
|
+
sessionId: session.id,
|
|
603
|
+
branchHead: index === 0,
|
|
604
|
+
children: [],
|
|
605
|
+
})));
|
|
606
|
+
}
|
|
607
|
+
// Linear links first: node[i].children = [node[i+1]].
|
|
608
|
+
for (const chain of chains.values()) {
|
|
609
|
+
for (let i = 0; i + 1 < chain.length; i++) {
|
|
610
|
+
chain[i].children.push(chain[i + 1]);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Fork attachments, oldest fork first at each anchor. The parent's own
|
|
614
|
+
// continuation (linked above) stays child #0 — the main branch.
|
|
615
|
+
//
|
|
616
|
+
// Anchor search walks UP the ancestor chain when the direct parent does
|
|
617
|
+
// not display the fork point: seed-prefix trimming moved those entries to
|
|
618
|
+
// an ancestor's chain (seqs are shared across the self-contained logs), so
|
|
619
|
+
// a fork whose boundary predates the parent's own entries correctly becomes
|
|
620
|
+
// a sibling of the branch it diverged from. Only when NO ancestor displays
|
|
621
|
+
// an entry at or before the boundary does the fork hang off a synthesized
|
|
622
|
+
// session-start head. Heads are synthesized in a separate pass BEFORE any
|
|
623
|
+
// attachment — attaching through a chain head that is later unshifted
|
|
624
|
+
// would leave the new head (and its subtree) disconnected.
|
|
625
|
+
const childrenOf = new Map();
|
|
626
|
+
for (const session of sessions) {
|
|
627
|
+
const parentId = parentOf(session);
|
|
628
|
+
if (parentId === undefined || !byId.has(parentId))
|
|
629
|
+
continue;
|
|
630
|
+
const siblings = childrenOf.get(parentId);
|
|
631
|
+
if (siblings === undefined)
|
|
632
|
+
childrenOf.set(parentId, [session]);
|
|
633
|
+
else
|
|
634
|
+
siblings.push(session);
|
|
635
|
+
}
|
|
636
|
+
/** Last displayed entry with seq <= boundary, walking up from the parent. */
|
|
637
|
+
const findAnchor = (fork) => {
|
|
638
|
+
const boundary = (fork.seedLength ?? 0) - 1;
|
|
639
|
+
const visited = new Set();
|
|
640
|
+
let cursorId = fork.parentSession;
|
|
641
|
+
while (cursorId !== undefined && !visited.has(cursorId)) {
|
|
642
|
+
visited.add(cursorId);
|
|
643
|
+
const holder = byId.get(cursorId);
|
|
644
|
+
if (holder === undefined)
|
|
645
|
+
break;
|
|
646
|
+
const chain = chains.get(cursorId);
|
|
647
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
648
|
+
const entry = chain[i].entry;
|
|
649
|
+
if (entry !== null && entry.seq <= boundary)
|
|
650
|
+
return chain[i];
|
|
651
|
+
}
|
|
652
|
+
cursorId = parentOf(holder);
|
|
653
|
+
}
|
|
654
|
+
return undefined;
|
|
655
|
+
};
|
|
656
|
+
// Pass A: resolve anchors, marking parents whose fork predates everything
|
|
657
|
+
// displayable (those need a session-start head).
|
|
658
|
+
const headlessParents = new Set();
|
|
659
|
+
const resolved = new Map();
|
|
660
|
+
for (const [parentId, forks] of childrenOf) {
|
|
661
|
+
const list = forks.sort((a, b) => a.createdAt - b.createdAt)
|
|
662
|
+
.map(fork => {
|
|
663
|
+
const anchor = findAnchor(fork);
|
|
664
|
+
if (anchor === undefined)
|
|
665
|
+
headlessParents.add(parentId);
|
|
666
|
+
return { anchor, fork };
|
|
667
|
+
});
|
|
668
|
+
resolved.set(parentId, list);
|
|
669
|
+
}
|
|
670
|
+
// Pass B: synthesize the session-start heads (skipped when the parent's
|
|
671
|
+
// chain already opens with a placeholder).
|
|
672
|
+
for (const parentId of headlessParents) {
|
|
673
|
+
const parentChain = chains.get(parentId);
|
|
674
|
+
const first = parentChain[0];
|
|
675
|
+
if (first.entry === null)
|
|
676
|
+
continue;
|
|
677
|
+
const head = { id: `${parentId}:head`, entry: null, sessionId: parentId, branchHead: true, children: [] };
|
|
678
|
+
first.branchHead = false;
|
|
679
|
+
head.children.push(first);
|
|
680
|
+
parentChain.unshift(head);
|
|
681
|
+
}
|
|
682
|
+
// Pass C: attach — chain heads are final now, no capture can go stale.
|
|
683
|
+
for (const [parentId, list] of resolved) {
|
|
684
|
+
for (const { anchor, fork } of list) {
|
|
685
|
+
const forkHead = chains.get(fork.id)[0];
|
|
686
|
+
(anchor ?? chains.get(parentId)[0]).children.push(forkHead);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
const roots = [];
|
|
690
|
+
for (const session of sessions) {
|
|
691
|
+
const parentId = parentOf(session);
|
|
692
|
+
if (parentId !== undefined && byId.has(parentId))
|
|
693
|
+
continue;
|
|
694
|
+
roots.push(chains.get(session.id)[0]);
|
|
695
|
+
}
|
|
696
|
+
// Active path: live session's whole chain, then each ancestor up to the
|
|
697
|
+
// fork boundary the next hop inherited. The boundary only NARROWS walking
|
|
698
|
+
// up: a descendant that forked inside its parent's INHERITED prefix caps
|
|
699
|
+
// the path at its own fork point — the parent's own entries past it (and
|
|
700
|
+
// the grandparent's entries past that) belong to dead branches, not the
|
|
701
|
+
// path to the live tip. Cycle-guarded (corrupt headers).
|
|
702
|
+
const activePath = new Set();
|
|
703
|
+
let activeLeafId = null;
|
|
704
|
+
const liveChain = chains.get(liveSessionId);
|
|
705
|
+
if (liveChain !== undefined) {
|
|
706
|
+
activeLeafId = liveChain.at(-1).id;
|
|
707
|
+
for (const node of liveChain)
|
|
708
|
+
activePath.add(node.id);
|
|
709
|
+
const visited = new Set([liveSessionId]);
|
|
710
|
+
let current = byId.get(liveSessionId);
|
|
711
|
+
let boundary = Number.POSITIVE_INFINITY;
|
|
712
|
+
while (current !== undefined) {
|
|
713
|
+
const parentId = parentOf(current);
|
|
714
|
+
if (parentId === undefined || !byId.has(parentId))
|
|
715
|
+
break;
|
|
716
|
+
if (visited.has(parentId))
|
|
717
|
+
break;
|
|
718
|
+
visited.add(parentId);
|
|
719
|
+
boundary = Math.min(boundary, (current.seedLength ?? 0) - 1);
|
|
720
|
+
for (const node of chains.get(parentId)) {
|
|
721
|
+
if (node.entry !== null && node.entry.seq <= boundary)
|
|
722
|
+
activePath.add(node.id);
|
|
723
|
+
}
|
|
724
|
+
current = byId.get(parentId);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return { roots, activePath, activeLeafId, sessions: metas, rewindFacts, truncated, sessionCount: sessions.length };
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Flatten the tree into render rows, pi-style: the subtree containing the
|
|
731
|
+
* active leaf sorts first at every branch point; single-child chains keep
|
|
732
|
+
* their indent, branch points indent +1 (plus the first generation after a
|
|
733
|
+
* branch); gutters record where ancestor │ lines continue.
|
|
734
|
+
*/
|
|
735
|
+
export function flattenTree(roots, activeLeafId) {
|
|
736
|
+
const multipleRoots = roots.length > 1;
|
|
737
|
+
// Post-order: which subtrees contain the active leaf (sorted first below).
|
|
738
|
+
const containsActive = new Map();
|
|
739
|
+
{
|
|
740
|
+
const all = [];
|
|
741
|
+
const stack = [...roots];
|
|
742
|
+
while (stack.length > 0) {
|
|
743
|
+
const node = stack.pop();
|
|
744
|
+
all.push(node);
|
|
745
|
+
for (let i = node.children.length - 1; i >= 0; i--)
|
|
746
|
+
stack.push(node.children[i]);
|
|
747
|
+
}
|
|
748
|
+
for (let i = all.length - 1; i >= 0; i--) {
|
|
749
|
+
const node = all[i];
|
|
750
|
+
let has = node.id === activeLeafId;
|
|
751
|
+
for (const child of node.children) {
|
|
752
|
+
if (containsActive.get(child) === true)
|
|
753
|
+
has = true;
|
|
754
|
+
}
|
|
755
|
+
containsActive.set(node, has);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
const activeFirst = (nodes) => {
|
|
759
|
+
const prioritized = [];
|
|
760
|
+
const rest = [];
|
|
761
|
+
for (const node of nodes) {
|
|
762
|
+
if (containsActive.get(node) === true)
|
|
763
|
+
prioritized.push(node);
|
|
764
|
+
else
|
|
765
|
+
rest.push(node);
|
|
766
|
+
}
|
|
767
|
+
return [...prioritized, ...rest];
|
|
768
|
+
};
|
|
769
|
+
const stack = [];
|
|
770
|
+
const orderedRoots = activeFirst(roots);
|
|
771
|
+
for (let i = orderedRoots.length - 1; i >= 0; i--) {
|
|
772
|
+
stack.push([
|
|
773
|
+
orderedRoots[i],
|
|
774
|
+
multipleRoots ? 1 : 0,
|
|
775
|
+
multipleRoots,
|
|
776
|
+
multipleRoots,
|
|
777
|
+
i === orderedRoots.length - 1,
|
|
778
|
+
[],
|
|
779
|
+
multipleRoots,
|
|
780
|
+
null,
|
|
781
|
+
]);
|
|
782
|
+
}
|
|
783
|
+
const flat = [];
|
|
784
|
+
while (stack.length > 0) {
|
|
785
|
+
const [node, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild, parentId] = stack.pop();
|
|
786
|
+
flat.push({
|
|
787
|
+
node,
|
|
788
|
+
parentId,
|
|
789
|
+
indent,
|
|
790
|
+
showConnector,
|
|
791
|
+
isLast,
|
|
792
|
+
gutters,
|
|
793
|
+
isVirtualRootChild,
|
|
794
|
+
multipleRoots,
|
|
795
|
+
});
|
|
796
|
+
const children = activeFirst(node.children);
|
|
797
|
+
const multipleChildren = children.length > 1;
|
|
798
|
+
let childIndent;
|
|
799
|
+
if (multipleChildren)
|
|
800
|
+
childIndent = indent + 1;
|
|
801
|
+
else if (justBranched && indent > 0)
|
|
802
|
+
childIndent = indent + 1;
|
|
803
|
+
else
|
|
804
|
+
childIndent = indent;
|
|
805
|
+
const connectorDisplayed = showConnector && !isVirtualRootChild;
|
|
806
|
+
const currentDisplayIndent = multipleRoots ? Math.max(0, indent - 1) : indent;
|
|
807
|
+
const connectorPosition = Math.max(0, currentDisplayIndent - 1);
|
|
808
|
+
const childGutters = connectorDisplayed
|
|
809
|
+
? [...gutters, { position: connectorPosition, show: !isLast }]
|
|
810
|
+
: gutters;
|
|
811
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
812
|
+
stack.push([
|
|
813
|
+
children[i],
|
|
814
|
+
childIndent,
|
|
815
|
+
multipleChildren,
|
|
816
|
+
multipleChildren,
|
|
817
|
+
i === children.length - 1,
|
|
818
|
+
childGutters,
|
|
819
|
+
false,
|
|
820
|
+
node.id,
|
|
821
|
+
]);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return flat;
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Filter + search the flattened tree, then recompute the tree drawing over
|
|
828
|
+
* the visible rows (each visible node re-hangs under its nearest visible
|
|
829
|
+
* ancestor, so hidden intermediates don't drift the indent). The active leaf
|
|
830
|
+
* always survives filtering (pi keeps the current position visible).
|
|
831
|
+
*/
|
|
832
|
+
export function filterTree(flat, activeLeafId, filter, query) {
|
|
833
|
+
const tokens = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
834
|
+
const visible = flat.filter(flatNode => {
|
|
835
|
+
const { node } = flatNode;
|
|
836
|
+
if (node.id === activeLeafId)
|
|
837
|
+
return true;
|
|
838
|
+
if (node.entry === null) {
|
|
839
|
+
// Placeholders: structure-only rows. `all` shows them; any search
|
|
840
|
+
// query hides them (no text to match) so they don't become stray
|
|
841
|
+
// roots; other modes drop them unless a visible descendant re-hangs.
|
|
842
|
+
return filter === 'all' && tokens.length === 0;
|
|
843
|
+
}
|
|
844
|
+
switch (filter) {
|
|
845
|
+
case 'user-only':
|
|
846
|
+
if (node.entry.kind !== 'user')
|
|
847
|
+
return false;
|
|
848
|
+
break;
|
|
849
|
+
case 'no-tools':
|
|
850
|
+
if (node.entry.kind === 'tool' || node.entry.kind === 'notice')
|
|
851
|
+
return false;
|
|
852
|
+
break;
|
|
853
|
+
case 'all':
|
|
854
|
+
break;
|
|
855
|
+
default:
|
|
856
|
+
if (node.entry.kind === 'notice')
|
|
857
|
+
return false;
|
|
858
|
+
break;
|
|
859
|
+
}
|
|
860
|
+
if (tokens.length > 0) {
|
|
861
|
+
const haystack = node.entry.searchText.toLowerCase();
|
|
862
|
+
return tokens.every(token => haystack.includes(token));
|
|
863
|
+
}
|
|
864
|
+
return true;
|
|
865
|
+
});
|
|
866
|
+
if (visible.length === 0)
|
|
867
|
+
return visible;
|
|
868
|
+
// Re-hang visible nodes under their nearest visible ancestor.
|
|
869
|
+
const visibleIds = new Set(visible.map(flatNode => flatNode.node.id));
|
|
870
|
+
const byNodeId = new Map(flat.map(flatNode => [flatNode.node.id, flatNode]));
|
|
871
|
+
const visibleParent = new Map();
|
|
872
|
+
const visibleChildren = new Map();
|
|
873
|
+
visibleChildren.set(null, []);
|
|
874
|
+
const nearestVisible = (id) => {
|
|
875
|
+
let current = byNodeId.get(id)?.parentId ?? null;
|
|
876
|
+
while (current !== null) {
|
|
877
|
+
if (visibleIds.has(current))
|
|
878
|
+
return current;
|
|
879
|
+
current = byNodeId.get(current)?.parentId ?? null;
|
|
880
|
+
}
|
|
881
|
+
return null;
|
|
882
|
+
};
|
|
883
|
+
for (const flatNode of visible) {
|
|
884
|
+
const ancestor = nearestVisible(flatNode.node.id);
|
|
885
|
+
visibleParent.set(flatNode.node.id, ancestor);
|
|
886
|
+
const siblings = visibleChildren.get(ancestor);
|
|
887
|
+
if (siblings === undefined)
|
|
888
|
+
visibleChildren.set(ancestor, [flatNode.node.id]);
|
|
889
|
+
else
|
|
890
|
+
siblings.push(flatNode.node.id);
|
|
891
|
+
}
|
|
892
|
+
// Recompute indent/connectors/gutters over the visible tree, same rules as
|
|
893
|
+
// flattenTree (roots = children of the virtual null parent).
|
|
894
|
+
const visibleRoots = visibleChildren.get(null);
|
|
895
|
+
const multipleRoots = visibleRoots.length > 1;
|
|
896
|
+
const visibleById = new Map(visible.map(flatNode => [flatNode.node.id, flatNode]));
|
|
897
|
+
const stack = [];
|
|
898
|
+
for (let i = visibleRoots.length - 1; i >= 0; i--) {
|
|
899
|
+
stack.push([visibleRoots[i], multipleRoots ? 1 : 0, multipleRoots, multipleRoots, i === visibleRoots.length - 1, [], multipleRoots]);
|
|
900
|
+
}
|
|
901
|
+
const out = [];
|
|
902
|
+
while (stack.length > 0) {
|
|
903
|
+
const [id, indent, justBranched, showConnector, isLast, gutters, isVirtualRootChild] = stack.pop();
|
|
904
|
+
const flatNode = visibleById.get(id);
|
|
905
|
+
flatNode.indent = indent;
|
|
906
|
+
flatNode.showConnector = showConnector;
|
|
907
|
+
flatNode.isLast = isLast;
|
|
908
|
+
flatNode.gutters = gutters;
|
|
909
|
+
flatNode.isVirtualRootChild = isVirtualRootChild;
|
|
910
|
+
flatNode.multipleRoots = multipleRoots;
|
|
911
|
+
out.push(flatNode);
|
|
912
|
+
const children = visibleChildren.get(id) ?? [];
|
|
913
|
+
const multipleChildren = children.length > 1;
|
|
914
|
+
let childIndent;
|
|
915
|
+
if (multipleChildren)
|
|
916
|
+
childIndent = indent + 1;
|
|
917
|
+
else if (justBranched && indent > 0)
|
|
918
|
+
childIndent = indent + 1;
|
|
919
|
+
else
|
|
920
|
+
childIndent = indent;
|
|
921
|
+
const connectorDisplayed = showConnector && !isVirtualRootChild;
|
|
922
|
+
const currentDisplayIndent = multipleRoots ? Math.max(0, indent - 1) : indent;
|
|
923
|
+
const connectorPosition = Math.max(0, currentDisplayIndent - 1);
|
|
924
|
+
const childGutters = connectorDisplayed
|
|
925
|
+
? [...gutters, { position: connectorPosition, show: !isLast }]
|
|
926
|
+
: gutters;
|
|
927
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
928
|
+
stack.push([children[i], childIndent, multipleChildren, multipleChildren, i === children.length - 1, childGutters, false]);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
return out;
|
|
932
|
+
}
|
|
933
|
+
/**
|
|
934
|
+
* Index of `targetId` in `visible`, walking the FULL list's parent chain when
|
|
935
|
+
* the exact node was filtered out; falls back to the last visible row
|
|
936
|
+
* (pi's findNearestVisibleIndex).
|
|
937
|
+
*/
|
|
938
|
+
export function nearestVisibleIndex(visible, full, targetId) {
|
|
939
|
+
if (visible.length === 0)
|
|
940
|
+
return 0;
|
|
941
|
+
const visibleIndexById = new Map(visible.map((flatNode, index) => [flatNode.node.id, index]));
|
|
942
|
+
const fullById = new Map(full.map(flatNode => [flatNode.node.id, flatNode]));
|
|
943
|
+
let current = targetId;
|
|
944
|
+
while (current !== null) {
|
|
945
|
+
const index = visibleIndexById.get(current);
|
|
946
|
+
if (index !== undefined)
|
|
947
|
+
return index;
|
|
948
|
+
const parent = fullById.get(current)?.parentId ?? null;
|
|
949
|
+
if (parent === current)
|
|
950
|
+
break; // corrupt self-parent guard
|
|
951
|
+
current = parent;
|
|
952
|
+
}
|
|
953
|
+
return visible.length - 1;
|
|
954
|
+
}
|