@deepseek-harness-tui/dsh-tui 0.8.0 → 0.8.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 +32 -15
- package/cordis.patch.yml +21 -2
- package/cordis.yml +12 -1
- package/dsh-ecosystem-spec/protocols/profile-definitions.d.ts +13 -0
- package/dsh-ecosystem-spec/protocols/profile-definitions.js +59 -0
- package/dsh-ecosystem-spec/protocols/tui-channel-http.d.ts +15 -0
- package/dsh-ecosystem-spec/protocols/tui-channel-http.js +32 -0
- package/dsh-ecosystem-spec/protocols/tui-channel.d.ts +59 -0
- package/dsh-ecosystem-spec/protocols/tui-channel.js +166 -0
- package/dsh-ecosystem-spec/protocols/tui-contributions.d.ts +42 -0
- package/dsh-ecosystem-spec/protocols/tui-contributions.js +143 -0
- package/dsh-ecosystem-spec/registry/README.md +14 -0
- package/dsh-ecosystem-spec/registry/contracts/decision-events-v1alpha1.json +13 -0
- package/dsh-ecosystem-spec/registry/contracts/scene-v1alpha1.json +13 -0
- package/dsh-ecosystem-spec/registry/contracts/settings-section-v1alpha1.json +13 -0
- package/dsh-ecosystem-spec/registry/contracts/tui-channel-v1alpha1.json +30 -0
- package/dsh-ecosystem-spec/registry/contracts/workspace-provider-v1alpha1.json +6 -0
- package/dsh-ecosystem-spec/registry/host-descriptor.tui.example.json +56 -0
- package/dsh-ecosystem-spec/registry/permissions-0.1.json +13 -0
- package/dsh-ecosystem-spec/registry/registry-0.15.json +149 -0
- package/dsh-ecosystem-spec/schemas/conformance-claim.schema.json +15 -0
- package/dsh-ecosystem-spec/schemas/effect-ledger-record.schema.json +16 -0
- package/dsh-ecosystem-spec/schemas/host-descriptor.schema.json +26 -0
- package/lib/types/cc/figures.d.ts +4 -0
- package/lib/types/cc/figures.d.ts.map +1 -1
- package/lib/types/cc/figures.js +4 -0
- package/lib/types/command-trees.d.ts +2 -1
- package/lib/types/command-trees.d.ts.map +1 -1
- package/lib/types/command-trees.js +1 -1
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +2 -0
- package/lib/types/components/ExtensionDialog.d.ts +40 -0
- package/lib/types/components/ExtensionDialog.d.ts.map +1 -0
- package/lib/types/components/ExtensionDialog.js +210 -0
- package/lib/types/components/HelpMenu.js +1 -1
- package/lib/types/components/LoadedContextPanel.d.ts +5 -5
- package/lib/types/components/LoadedContextPanel.js +6 -6
- package/lib/types/components/LogoV2.d.ts.map +1 -1
- package/lib/types/components/LogoV2.js +16 -8
- package/lib/types/components/Markdown.d.ts +13 -1
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +15 -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 +111 -6
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +26 -12
- package/lib/types/components/RewindPicker.d.ts +13 -1
- package/lib/types/components/RewindPicker.d.ts.map +1 -1
- package/lib/types/components/RewindPicker.js +26 -2
- package/lib/types/components/StreamingMarkdown.d.ts +2 -8
- package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
- package/lib/types/components/StreamingMarkdown.js +51 -9
- package/lib/types/components/ToolUseLoader.d.ts +8 -3
- package/lib/types/components/ToolUseLoader.d.ts.map +1 -1
- package/lib/types/components/ToolUseLoader.js +43 -9
- package/lib/types/components/activityFrames.d.ts +1 -1
- package/lib/types/components/activityFrames.d.ts.map +1 -1
- package/lib/types/components/activityFrames.js +4 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts +5 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +26 -3
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +13 -2
- package/lib/types/dsh-adapter/channel.d.ts +61 -11
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +767 -120
- package/lib/types/dsh-adapter/childStderr.d.ts.map +1 -1
- package/lib/types/dsh-adapter/childStderr.js +10 -0
- package/lib/types/dsh-adapter/command-attribution.d.ts +51 -0
- package/lib/types/dsh-adapter/command-attribution.d.ts.map +1 -0
- package/lib/types/dsh-adapter/command-attribution.js +93 -0
- package/lib/types/dsh-adapter/command-errors.d.ts +42 -0
- package/lib/types/dsh-adapter/command-errors.d.ts.map +1 -0
- package/lib/types/dsh-adapter/command-errors.js +63 -0
- package/lib/types/dsh-adapter/command-trees.d.ts +7 -1
- package/lib/types/dsh-adapter/command-trees.d.ts.map +1 -1
- package/lib/types/dsh-adapter/command-trees.js +70 -20
- package/lib/types/dsh-adapter/component-identity.d.ts +33 -0
- package/lib/types/dsh-adapter/component-identity.d.ts.map +1 -0
- package/lib/types/dsh-adapter/component-identity.js +108 -0
- package/lib/types/dsh-adapter/contract.d.ts +1 -1
- package/lib/types/dsh-adapter/contract.js +2 -2
- package/lib/types/dsh-adapter/decision-guard.d.ts +84 -0
- package/lib/types/dsh-adapter/decision-guard.d.ts.map +1 -0
- package/lib/types/dsh-adapter/decision-guard.js +348 -0
- package/lib/types/dsh-adapter/dialogs.d.ts +145 -0
- package/lib/types/dsh-adapter/dialogs.d.ts.map +1 -0
- package/lib/types/dsh-adapter/dialogs.js +316 -0
- package/lib/types/dsh-adapter/effect-ledger.d.ts +91 -0
- package/lib/types/dsh-adapter/effect-ledger.d.ts.map +1 -0
- package/lib/types/dsh-adapter/effect-ledger.js +251 -0
- package/lib/types/dsh-adapter/extension-events.d.ts +193 -0
- package/lib/types/dsh-adapter/extension-events.d.ts.map +1 -0
- package/lib/types/dsh-adapter/extension-events.js +207 -0
- package/lib/types/dsh-adapter/extensions.d.ts +32 -0
- package/lib/types/dsh-adapter/extensions.d.ts.map +1 -0
- package/lib/types/dsh-adapter/extensions.js +42 -0
- package/lib/types/dsh-adapter/grants.d.ts +25 -0
- package/lib/types/dsh-adapter/grants.d.ts.map +1 -0
- package/lib/types/dsh-adapter/grants.js +213 -0
- package/lib/types/dsh-adapter/host-access.d.ts +37 -0
- package/lib/types/dsh-adapter/host-access.d.ts.map +1 -0
- package/lib/types/dsh-adapter/host-access.js +652 -0
- package/lib/types/dsh-adapter/host-descriptor.d.ts +24 -0
- package/lib/types/dsh-adapter/host-descriptor.d.ts.map +1 -0
- package/lib/types/dsh-adapter/host-descriptor.js +169 -0
- package/lib/types/dsh-adapter/index.d.ts +4 -0
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +1 -0
- package/lib/types/dsh-adapter/message-observer.d.ts +173 -0
- package/lib/types/dsh-adapter/message-observer.d.ts.map +1 -0
- package/lib/types/dsh-adapter/message-observer.js +553 -0
- package/lib/types/dsh-adapter/plugin-host.d.ts +136 -0
- package/lib/types/dsh-adapter/plugin-host.d.ts.map +1 -0
- package/lib/types/dsh-adapter/plugin-host.js +365 -0
- package/lib/types/dsh-adapter/plugin-storage.d.ts +108 -0
- package/lib/types/dsh-adapter/plugin-storage.d.ts.map +1 -0
- package/lib/types/dsh-adapter/plugin-storage.js +397 -0
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +124 -14
- package/lib/types/dsh-adapter/plugins-info.d.ts +43 -0
- package/lib/types/dsh-adapter/plugins-info.d.ts.map +1 -0
- package/lib/types/dsh-adapter/plugins-info.js +276 -0
- package/lib/types/dsh-adapter/promptDebug.d.ts +0 -6
- package/lib/types/dsh-adapter/promptDebug.d.ts.map +1 -1
- package/lib/types/dsh-adapter/promptDebug.js +13 -4
- package/lib/types/dsh-adapter/renderers.d.ts +57 -0
- package/lib/types/dsh-adapter/renderers.d.ts.map +1 -0
- package/lib/types/dsh-adapter/renderers.js +173 -0
- package/lib/types/dsh-adapter/sanitize.d.ts +36 -0
- package/lib/types/dsh-adapter/sanitize.d.ts.map +1 -0
- package/lib/types/dsh-adapter/sanitize.js +65 -0
- package/lib/types/dsh-adapter/scenes.d.ts +15 -5
- package/lib/types/dsh-adapter/scenes.d.ts.map +1 -1
- package/lib/types/dsh-adapter/scenes.js +149 -35
- package/lib/types/dsh-adapter/settings-sections.d.ts +7 -3
- package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -1
- package/lib/types/dsh-adapter/settings-sections.js +106 -26
- package/lib/types/dsh-adapter/shortcuts.d.ts +107 -0
- package/lib/types/dsh-adapter/shortcuts.d.ts.map +1 -0
- package/lib/types/dsh-adapter/shortcuts.js +340 -0
- package/lib/types/dsh-adapter/status.d.ts +74 -0
- package/lib/types/dsh-adapter/status.d.ts.map +1 -0
- package/lib/types/dsh-adapter/status.js +234 -0
- package/lib/types/dsh-adapter/workspaces.d.ts +16 -5
- package/lib/types/dsh-adapter/workspaces.d.ts.map +1 -1
- package/lib/types/dsh-adapter/workspaces.js +287 -170
- package/lib/types/extensions.d.ts +13 -0
- package/lib/types/extensions.d.ts.map +1 -0
- package/lib/types/extensions.js +21 -0
- package/lib/types/force-production-react.d.ts +1 -0
- package/lib/types/force-production-react.d.ts.map +1 -0
- package/lib/types/force-production-react.js +12 -0
- package/lib/types/i18n.d.ts +174 -20
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +56 -6
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +1 -0
- package/lib/types/ink/components/App.d.ts +2 -0
- package/lib/types/ink/components/App.d.ts.map +1 -1
- package/lib/types/ink/components/App.js +4 -0
- package/lib/types/ink/ink.d.ts +31 -3
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +90 -5
- package/lib/types/ink/log-update.d.ts.map +1 -1
- package/lib/types/ink/log-update.js +225 -56
- package/lib/types/ink/output.d.ts +35 -2
- package/lib/types/ink/output.d.ts.map +1 -1
- package/lib/types/ink/output.js +281 -14
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +14 -6
- package/lib/types/ink/screen.d.ts +39 -0
- package/lib/types/ink/screen.d.ts.map +1 -1
- package/lib/types/ink/screen.js +117 -0
- package/lib/types/ink/terminal.d.ts.map +1 -1
- package/lib/types/ink/terminal.js +26 -6
- package/lib/types/ink/wrapAnsi.d.ts +1 -1
- package/lib/types/ink/wrapAnsi.d.ts.map +1 -1
- package/lib/types/ink/wrapAnsi.js +29 -2
- package/lib/types/plugin-host.d.ts +15 -0
- package/lib/types/plugin-host.d.ts.map +1 -0
- package/lib/types/plugin-host.js +17 -0
- package/lib/types/plugin-spec/negotiate.d.ts +11 -0
- package/lib/types/plugin-spec/negotiate.d.ts.map +1 -0
- package/lib/types/plugin-spec/negotiate.js +97 -0
- package/lib/types/plugin-spec/permission-scope.d.ts +32 -0
- package/lib/types/plugin-spec/permission-scope.d.ts.map +1 -0
- package/lib/types/plugin-spec/permission-scope.js +90 -0
- package/lib/types/plugin-spec/registry.d.ts +23 -0
- package/lib/types/plugin-spec/registry.d.ts.map +1 -0
- package/lib/types/plugin-spec/registry.js +252 -0
- package/lib/types/plugin-spec/schema-check.d.ts +17 -0
- package/lib/types/plugin-spec/schema-check.d.ts.map +1 -0
- package/lib/types/plugin-spec/schema-check.js +165 -0
- package/lib/types/plugin-spec/tui-extension.d.ts +16 -0
- package/lib/types/plugin-spec/tui-extension.d.ts.map +1 -0
- package/lib/types/plugin-spec/tui-extension.js +79 -0
- package/lib/types/plugin-spec/types.d.ts +114 -0
- package/lib/types/plugin-spec/types.d.ts.map +1 -0
- package/lib/types/plugin-spec/types.js +10 -0
- package/lib/types/plugin-spec/validate.d.ts +21 -0
- package/lib/types/plugin-spec/validate.d.ts.map +1 -0
- package/lib/types/plugin-spec/validate.js +138 -0
- package/lib/types/scenes.d.ts +2 -1
- package/lib/types/scenes.d.ts.map +1 -1
- package/lib/types/scenes.js +1 -1
- package/lib/types/screens/Chat.d.ts +14 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +173 -49
- package/lib/types/screens/SessionBrowser.d.ts.map +1 -1
- package/lib/types/screens/SessionBrowser.js +10 -3
- package/lib/types/sessions/view.d.ts +3 -2
- package/lib/types/sessions/view.d.ts.map +1 -1
- package/lib/types/sessions/view.js +18 -0
- package/lib/types/settings-sections.d.ts +2 -1
- package/lib/types/settings-sections.d.ts.map +1 -1
- package/lib/types/settings-sections.js +1 -1
- package/lib/types/theme.d.ts +5 -0
- package/lib/types/theme.d.ts.map +1 -1
- package/lib/types/theme.js +15 -0
- package/lib/types/update.d.ts +20 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +54 -7
- package/lib/types/utils/execFileNoThrow.d.ts.map +1 -1
- package/lib/types/utils/execFileNoThrow.js +3 -0
- package/lib/types/utils/loaded-context.d.ts +8 -3
- package/lib/types/utils/loaded-context.d.ts.map +1 -1
- package/lib/types/utils/loaded-context.js +39 -3
- package/lib/types/utils/modifiers.d.ts.map +1 -1
- package/lib/types/utils/modifiers.js +3 -2
- package/lib/types/working-activity.d.ts +12 -0
- package/lib/types/working-activity.d.ts.map +1 -1
- package/lib/types/working-activity.js +13 -0
- package/lib/types/workspaces.d.ts +2 -1
- package/lib/types/workspaces.d.ts.map +1 -1
- package/lib/types/workspaces.js +1 -1
- package/node_modules/@dsh-std/command/CHANGELOG.md +8 -0
- package/node_modules/@dsh-std/command/LICENSE +21 -0
- package/node_modules/@dsh-std/command/README.md +26 -0
- package/node_modules/@dsh-std/command/README.zh.md +28 -0
- package/node_modules/@dsh-std/command/lib/index.d.ts +144 -0
- package/node_modules/@dsh-std/command/lib/index.d.ts.map +1 -0
- package/node_modules/@dsh-std/command/lib/index.js +252 -0
- package/node_modules/@dsh-std/command/lib/index.js.map +1 -0
- package/node_modules/@dsh-std/command/package.json +22 -0
- package/node_modules/@dsh-std/connection/CHANGELOG.md +8 -0
- package/node_modules/@dsh-std/connection/LICENSE +21 -0
- package/node_modules/@dsh-std/connection/README.md +42 -0
- package/node_modules/@dsh-std/connection/README.zh.md +42 -0
- package/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts +125 -0
- package/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts.map +1 -0
- package/node_modules/@dsh-std/connection/lib/index.d.ts +89 -0
- package/node_modules/@dsh-std/connection/lib/index.d.ts.map +1 -0
- package/node_modules/@dsh-std/connection/lib/index.js +158 -0
- package/node_modules/@dsh-std/connection/lib/index.js.map +1 -0
- package/node_modules/@dsh-std/connection/lib/memory.d.ts +24 -0
- package/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -0
- package/node_modules/@dsh-std/connection/lib/memory.js +258 -0
- package/node_modules/@dsh-std/connection/lib/memory.js.map +1 -0
- package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js +226 -0
- package/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -0
- package/node_modules/@dsh-std/connection/package.json +20 -0
- package/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
- package/node_modules/@dsh-std/core/LICENSE +21 -0
- package/node_modules/@dsh-std/core/README.md +13 -0
- package/node_modules/@dsh-std/core/README.zh.md +13 -0
- package/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
- package/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
- package/node_modules/@dsh-std/core/lib/index.js +424 -0
- package/node_modules/@dsh-std/core/lib/index.js.map +1 -0
- package/node_modules/@dsh-std/core/package.json +21 -0
- package/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -0
- package/node_modules/@dsh-std/manifest/LICENSE +21 -0
- package/node_modules/@dsh-std/manifest/README.md +5 -0
- package/node_modules/@dsh-std/manifest/README.zh.md +5 -0
- package/node_modules/@dsh-std/manifest/lib/index.d.ts +182 -0
- package/node_modules/@dsh-std/manifest/lib/index.d.ts.map +1 -0
- package/node_modules/@dsh-std/manifest/lib/index.js +625 -0
- package/node_modules/@dsh-std/manifest/lib/index.js.map +1 -0
- package/node_modules/@dsh-std/manifest/package.json +20 -0
- package/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -0
- package/node_modules/@dsh-std/messages/CHANGELOG.md +8 -0
- package/node_modules/@dsh-std/messages/LICENSE +21 -0
- package/node_modules/@dsh-std/messages/README.md +5 -0
- package/node_modules/@dsh-std/messages/README.zh.md +7 -0
- package/node_modules/@dsh-std/messages/lib/index.d.ts +50 -0
- package/node_modules/@dsh-std/messages/lib/index.d.ts.map +1 -0
- package/node_modules/@dsh-std/messages/lib/index.js +151 -0
- package/node_modules/@dsh-std/messages/lib/index.js.map +1 -0
- package/node_modules/@dsh-std/messages/package.json +20 -0
- package/node_modules/@dsh-std/messages/schema/message-observer-envelope-0.15.schema.json +54 -0
- package/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -0
- package/node_modules/@dsh-std/presentation/LICENSE +21 -0
- package/node_modules/@dsh-std/presentation/README.md +27 -0
- package/node_modules/@dsh-std/presentation/README.zh.md +29 -0
- package/node_modules/@dsh-std/presentation/lib/callback.d.ts +2 -0
- package/node_modules/@dsh-std/presentation/lib/callback.js +179 -0
- package/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -0
- package/node_modules/@dsh-std/presentation/lib/index.d.ts +262 -0
- package/node_modules/@dsh-std/presentation/lib/index.d.ts.map +1 -0
- package/node_modules/@dsh-std/presentation/lib/index.js +550 -0
- package/node_modules/@dsh-std/presentation/lib/index.js.map +1 -0
- package/node_modules/@dsh-std/presentation/lib/interaction.d.ts +2 -0
- package/node_modules/@dsh-std/presentation/lib/interaction.js +2 -0
- package/node_modules/@dsh-std/presentation/lib/operations.d.ts +2 -0
- package/node_modules/@dsh-std/presentation/lib/operations.js +2 -0
- package/node_modules/@dsh-std/presentation/package.json +25 -0
- package/node_modules/@dsh-std/storage/CHANGELOG.md +8 -0
- package/node_modules/@dsh-std/storage/LICENSE +21 -0
- package/node_modules/@dsh-std/storage/README.md +5 -0
- package/node_modules/@dsh-std/storage/README.zh.md +7 -0
- package/node_modules/@dsh-std/storage/lib/index.d.ts +48 -0
- package/node_modules/@dsh-std/storage/lib/index.d.ts.map +1 -0
- package/node_modules/@dsh-std/storage/lib/index.js +104 -0
- package/node_modules/@dsh-std/storage/lib/index.js.map +1 -0
- package/node_modules/@dsh-std/storage/package.json +20 -0
- package/node_modules/@dsh-std/storage/schema/local-storage-0.15.schema.json +56 -0
- package/package.json +110 -70
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/CHANGELOG.md +8 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/LICENSE +21 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.md +42 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/README.zh.md +42 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts +125 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.d.ts +89 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.d.ts.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js +158 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/index.js.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.d.ts +24 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js +258 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/memory.js.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js +226 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/connection/package.json +20 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/LICENSE +21 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.md +13 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/README.zh.md +13 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js +424 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/lib/index.js.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/core/package.json +21 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/CHANGELOG.md +9 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/LICENSE +21 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.md +5 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/README.zh.md +5 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.d.ts +182 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.d.ts.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js +625 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/lib/index.js.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/package.json +20 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/manifest/schema/dsh-plugin-0.15.schema.json +144 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/CHANGELOG.md +9 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/LICENSE +21 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.md +27 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/README.zh.md +29 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.d.ts +2 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js +179 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/callback.js.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.d.ts +262 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.d.ts.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js +550 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/index.js.map +1 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/interaction.d.ts +2 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/interaction.js +2 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/operations.d.ts +2 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/lib/operations.js +2 -0
- package/vendor/dsh-std/packages/command/node_modules/@dsh-std/presentation/package.json +25 -0
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/LICENSE +21 -0
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.md +13 -0
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/README.zh.md +13 -0
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js +424 -0
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/lib/index.js.map +1 -0
- package/vendor/dsh-std/packages/connection/node_modules/@dsh-std/core/package.json +21 -0
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/LICENSE +21 -0
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.md +13 -0
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/README.zh.md +13 -0
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js +424 -0
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/lib/index.js.map +1 -0
- package/vendor/dsh-std/packages/manifest/node_modules/@dsh-std/core/package.json +21 -0
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/LICENSE +21 -0
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.md +13 -0
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/README.zh.md +13 -0
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js +424 -0
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/lib/index.js.map +1 -0
- package/vendor/dsh-std/packages/messages/node_modules/@dsh-std/core/package.json +21 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/CHANGELOG.md +8 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/LICENSE +21 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.md +42 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/README.zh.md +42 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts +125 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/connection-CdGcSQnu.d.ts.map +1 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.d.ts +89 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.d.ts.map +1 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js +158 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/index.js.map +1 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.d.ts +24 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.d.ts.map +1 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js +258 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/memory.js.map +1 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js +226 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/lib/resolve-3X6ausgz.js.map +1 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/connection/package.json +20 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/LICENSE +21 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.md +13 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/README.zh.md +13 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js +424 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/lib/index.js.map +1 -0
- package/vendor/dsh-std/packages/presentation/node_modules/@dsh-std/core/package.json +21 -0
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/CHANGELOG.md +8 -0
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/LICENSE +21 -0
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.md +13 -0
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/README.zh.md +13 -0
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.d.ts +112 -0
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.d.ts.map +1 -0
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js +424 -0
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/lib/index.js.map +1 -0
- package/vendor/dsh-std/packages/storage/node_modules/@dsh-std/core/package.json +21 -0
|
@@ -26,7 +26,108 @@ import { t } from '../i18n.js';
|
|
|
26
26
|
import { modeDisplayName, resolveSessionModes } from '../sessionModes.js';
|
|
27
27
|
import { ActivityTracker } from 'dsh-working-activity/status';
|
|
28
28
|
import { attachSessionToWorkspace } from './workspace.js';
|
|
29
|
-
import { createLocalWorkspaceRuntime } from './workspaces.js';
|
|
29
|
+
import { createLocalWorkspaceRuntime, getHostWorkspaceRuntime } from './workspaces.js';
|
|
30
|
+
import { getHostCommandTrees } from './command-trees.js';
|
|
31
|
+
import { getHostSettingsSections } from './settings-sections.js';
|
|
32
|
+
import { getHostSceneRuntime } from './scenes.js';
|
|
33
|
+
import { getHostRenderers } from './renderers.js';
|
|
34
|
+
import { getHostMessageObserver } from './message-observer.js';
|
|
35
|
+
import { dispatchTuiDecision, dispatchTuiNotification, normalizeCancelDecision } from './extension-events.js';
|
|
36
|
+
import { installDecisionGuard } from './decision-guard.js';
|
|
37
|
+
import { commandOwner } from './command-attribution.js';
|
|
38
|
+
import { readGrantStore } from './grants.js';
|
|
39
|
+
import { hasCommandErrorCode, mapCommandError } from './command-errors.js';
|
|
40
|
+
import { pluginsInfoLines } from './plugins-info.js';
|
|
41
|
+
import { cleanRenderText, cleanScalarText } from './sanitize.js';
|
|
42
|
+
/** `tui/input` return normalization: transform/handled/cancel or no opinion.
|
|
43
|
+
* A blank `{ text }` rewrite is NOT a decision — it is logged and the chain
|
|
44
|
+
* continues so a later veto listener still runs. */
|
|
45
|
+
function normalizeInputDecision(result, warn) {
|
|
46
|
+
if (result === undefined || result === null || result === false)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (typeof result !== 'object') {
|
|
49
|
+
warn(`a non-object (${typeof result})`);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
const record = result;
|
|
53
|
+
if (record.cancel === true) {
|
|
54
|
+
const reason = cleanScalarText(record.reason, NOTICE_CELLS);
|
|
55
|
+
return { cancel: true, ...(reason === '' ? {} : { reason }) };
|
|
56
|
+
}
|
|
57
|
+
if (record.handled === true) {
|
|
58
|
+
const notice = cleanScalarText(record.notice, NOTICE_CELLS);
|
|
59
|
+
return { handled: true, ...(notice === '' ? {} : { notice }) };
|
|
60
|
+
}
|
|
61
|
+
if (typeof record.text === 'string') {
|
|
62
|
+
if (record.text.trim() === '') {
|
|
63
|
+
warn('a blank {text} rewrite');
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return { text: record.text };
|
|
67
|
+
}
|
|
68
|
+
warn('an unrecognized decision shape');
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
/** `tui/rewind-prompt` return normalization: cancel/modes or no opinion.
|
|
72
|
+
* Modes are COPIED with only validated, sanitized scalar fields — the raw
|
|
73
|
+
* plugin object must never reach the render path (a `description: {}` would
|
|
74
|
+
* crash ListItem's `.replace`, and control chars would corrupt the pane).
|
|
75
|
+
* Notices/reasons are toast-bound plugin text: sanitized too (see
|
|
76
|
+
* ./sanitize.js — the one implementation of the render-path contract). */
|
|
77
|
+
function normalizeRewindPromptDecision(result, warn) {
|
|
78
|
+
if (result === undefined || result === null || result === false)
|
|
79
|
+
return undefined;
|
|
80
|
+
if (typeof result !== 'object') {
|
|
81
|
+
warn(`a non-object (${typeof result})`);
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
const record = result;
|
|
85
|
+
if (record.cancel === true) {
|
|
86
|
+
const reason = cleanScalarText(record.reason, NOTICE_CELLS);
|
|
87
|
+
return { cancel: true, ...(reason === '' ? {} : { reason }) };
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(record.modes)) {
|
|
90
|
+
const modes = [];
|
|
91
|
+
for (const raw of record.modes) {
|
|
92
|
+
if (modes.length >= 8)
|
|
93
|
+
break;
|
|
94
|
+
if (raw === null || typeof raw !== 'object')
|
|
95
|
+
continue;
|
|
96
|
+
const candidate = raw;
|
|
97
|
+
if (typeof candidate.id !== 'string' || candidate.id.trim() === '')
|
|
98
|
+
continue;
|
|
99
|
+
const label = typeof candidate.label === 'string' ? cleanRenderText(candidate.label, 120) : '';
|
|
100
|
+
if (label === '')
|
|
101
|
+
continue;
|
|
102
|
+
const description = typeof candidate.description === 'string' && candidate.description.trim() !== ''
|
|
103
|
+
? cleanRenderText(candidate.description, 400)
|
|
104
|
+
: undefined;
|
|
105
|
+
modes.push({ id: candidate.id, label, ...(description === undefined ? {} : { description }) });
|
|
106
|
+
}
|
|
107
|
+
if (modes.length === 0) {
|
|
108
|
+
warn('an empty or invalid {modes} list');
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return { modes };
|
|
112
|
+
}
|
|
113
|
+
warn('an unrecognized decision shape');
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
/** Toast-bound plugin text (veto reasons, handled notices, rewind summaries)
|
|
117
|
+
* is render-path data too: same sanitization, toast-width cap. */
|
|
118
|
+
const NOTICE_CELLS = 200;
|
|
119
|
+
/** `tui/rewind-done` return normalization: the first non-empty STRING is the
|
|
120
|
+
* summary; anything else is not a decision. */
|
|
121
|
+
function normalizeRewindDoneSummary(result, warn) {
|
|
122
|
+
if (result === undefined || result === null || result === false)
|
|
123
|
+
return undefined;
|
|
124
|
+
if (typeof result === 'string') {
|
|
125
|
+
const summary = cleanRenderText(result, NOTICE_CELLS);
|
|
126
|
+
return summary === '' ? undefined : summary;
|
|
127
|
+
}
|
|
128
|
+
warn('a non-string summary');
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
30
131
|
/**
|
|
31
132
|
* Delay before re-reading a skill catalog that reported an incomplete
|
|
32
133
|
* observation (a provider whose directory watcher is still warming).
|
|
@@ -59,12 +160,25 @@ function preview(text, limit) {
|
|
|
59
160
|
* a loadOlder() restore is not instantly undone. Returns the number of rows
|
|
60
161
|
* folded.
|
|
61
162
|
*/
|
|
62
|
-
function foldRows(rows, cap) {
|
|
163
|
+
function foldRows(rows, cap, cursor) {
|
|
63
164
|
const excess = rows.length - cap;
|
|
64
|
-
if (excess <= 0)
|
|
165
|
+
if (excess <= 0) {
|
|
166
|
+
if (cursor !== undefined)
|
|
167
|
+
cursor.index = 0;
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
// Incremental pass: rows only ever append past the fold line and the
|
|
171
|
+
// folded/restored exemptions are permanent, so everything below a cursor
|
|
172
|
+
// over the SAME array identity needs no re-inspection. emit/emitStream
|
|
173
|
+
// fold on every frame during streaming — a full rescan of a long window
|
|
174
|
+
// there was the O(rows) per-frame term of long-session streaming.
|
|
175
|
+
const from = cursor === undefined ? 0 : cursor.rows === rows ? cursor.index : 0;
|
|
176
|
+
if (cursor !== undefined)
|
|
177
|
+
cursor.rows = rows;
|
|
178
|
+
if (excess <= from)
|
|
65
179
|
return 0;
|
|
66
180
|
let folded = 0;
|
|
67
|
-
for (const row of rows.slice(
|
|
181
|
+
for (const row of rows.slice(from, excess)) {
|
|
68
182
|
if (row.folded || row.restored)
|
|
69
183
|
continue;
|
|
70
184
|
if (row.kind !== 'user' && row.kind !== 'assistant' && row.kind !== 'reasoning' && row.kind !== 'tool')
|
|
@@ -86,6 +200,8 @@ function foldRows(rows, cap) {
|
|
|
86
200
|
row.text = preview(row.text, 200);
|
|
87
201
|
}
|
|
88
202
|
}
|
|
203
|
+
if (cursor !== undefined)
|
|
204
|
+
cursor.index = excess;
|
|
89
205
|
return folded;
|
|
90
206
|
}
|
|
91
207
|
/**
|
|
@@ -214,46 +330,44 @@ function restoreToolResult(row, event) {
|
|
|
214
330
|
row.tool.resultFull = result || undefined;
|
|
215
331
|
}
|
|
216
332
|
/**
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
* events
|
|
220
|
-
*
|
|
221
|
-
*
|
|
222
|
-
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
333
|
+
* Prepare durable events for REPLAY (resume / rewind / model-switch fork):
|
|
334
|
+
* drop settled `assistant/chunk` stream deltas — the sealed
|
|
335
|
+
* `assistant/message` events carry the full text and reasoning blocks, so
|
|
336
|
+
* per-token chunks add nothing to the replayed transcript while costing a
|
|
337
|
+
* per-chunk renderEvent pass (a real 4.5MB session logs ~19k chunks against
|
|
338
|
+
* ~30 messages). The trailing chunk run AFTER the last message belongs to an
|
|
339
|
+
* unfinished step (crash-orphaned turn) and is kept, so a resumed session
|
|
340
|
+
* still shows its partial content. Storage-level packed rows
|
|
341
|
+
* (`text-chunks`/`reasoning-chunks`/`tool-call-chunks`) are dropped the
|
|
342
|
+
* same way — defensive: the jsonl reader expands them, but a future
|
|
343
|
+
* direct-pass path must not resurrect them. Replay-side tps sampling is
|
|
344
|
+
* lost with the chunks (a live metric; lastUsage comes from the message's
|
|
345
|
+
* own usage). Live events never go through this.
|
|
225
346
|
*/
|
|
226
|
-
function
|
|
227
|
-
|
|
228
|
-
let run = null;
|
|
229
|
-
const flush = () => {
|
|
230
|
-
if (run === null)
|
|
231
|
-
return;
|
|
232
|
-
const chunk = run.event.data.chunk;
|
|
233
|
-
out.push({
|
|
234
|
-
...run.event,
|
|
235
|
-
data: { ...run.event.data, chunk: { ...chunk, text: run.parts.join('') } },
|
|
236
|
-
});
|
|
237
|
-
run = null;
|
|
238
|
-
};
|
|
347
|
+
function prepareReplayEvents(events) {
|
|
348
|
+
let lastMessageSeq = -1;
|
|
239
349
|
for (const event of events) {
|
|
240
|
-
if (event.type === 'assistant/
|
|
241
|
-
|
|
242
|
-
if (run !== null && run.type === event.data.chunk.type) {
|
|
243
|
-
// oxlint-disable-next-line typescript/no-unnecessary-condition -- durable replay data may lack text
|
|
244
|
-
run.parts.push(event.data.chunk.text ?? '');
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
flush();
|
|
248
|
-
// oxlint-disable-next-line typescript/no-unnecessary-condition -- durable replay data may lack text
|
|
249
|
-
run = { event, type: event.data.chunk.type, parts: [event.data.chunk.text ?? ''] };
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
flush();
|
|
253
|
-
out.push(event);
|
|
350
|
+
if (event.type === 'assistant/message')
|
|
351
|
+
lastMessageSeq = event.seq;
|
|
254
352
|
}
|
|
255
|
-
|
|
256
|
-
|
|
353
|
+
return events.filter(event => {
|
|
354
|
+
if (event.type === 'assistant/message')
|
|
355
|
+
return true;
|
|
356
|
+
if (event.type === 'assistant/chunk') {
|
|
357
|
+
// Keep only the in-flight tail (no message sealed after it).
|
|
358
|
+
return lastMessageSeq < 0 || event.seq > lastMessageSeq;
|
|
359
|
+
}
|
|
360
|
+
// Storage-level packed rows: not in the SessionEvent union (they exist
|
|
361
|
+
// only in the durable JSON), so compare through a widened view — the
|
|
362
|
+
// defensive drop is exactly for data the static type doesn't know.
|
|
363
|
+
const packedType = event.type;
|
|
364
|
+
if (packedType === 'text-chunks' ||
|
|
365
|
+
packedType === 'reasoning-chunks' ||
|
|
366
|
+
packedType === 'tool-call-chunks') {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
return true;
|
|
370
|
+
});
|
|
257
371
|
}
|
|
258
372
|
/** Buffer below the context window at which CC warns (autoCompact.ts). */
|
|
259
373
|
const CONTEXT_WARNING_BUFFER_TOKENS = 20_000;
|
|
@@ -288,18 +402,35 @@ async function waitForTurnEnd(session, fromSeq, timeoutMs) {
|
|
|
288
402
|
export function createChannel(ctx, initialAgent, options) {
|
|
289
403
|
let agent = initialAgent;
|
|
290
404
|
let currentHandle = options.handle;
|
|
405
|
+
// D-7 backstop: the extensions row installs the decision-subscription
|
|
406
|
+
// gate, but the channel IS the dispatch path — a stale patch without that
|
|
407
|
+
// row (or a bare embed mounting neither) would otherwise leave tui/input
|
|
408
|
+
// & friends subscribable by default, silently voiding the default-deny
|
|
409
|
+
// posture. Idempotent per cordis root, so the full-patch path installs
|
|
410
|
+
// exactly once whichever side runs first. One store instance serves both
|
|
411
|
+
// the gate and the invoke checkpoint below.
|
|
412
|
+
// Keep a private fallback for bare embedders, but resolve the host-owned
|
|
413
|
+
// store on every operation so a plugin-host row mounted later (or a custom
|
|
414
|
+
// live GrantStore) is not shadowed by an early snapshot.
|
|
415
|
+
const fallbackGrantStore = readGrantStore();
|
|
416
|
+
const currentGrantStore = () => ctx.get('tuiPluginHost')?.grants ?? fallbackGrantStore;
|
|
417
|
+
installDecisionGuard(ctx, currentGrantStore());
|
|
291
418
|
// The DSH slash-command registry (optional service): /plan, /goal and
|
|
292
419
|
// friends register here; the TUI merges their descriptors into the slash
|
|
293
420
|
// menu and dispatches through `execute` (which logs the paired
|
|
294
421
|
// command/run + command/done records). Absent the service, only the
|
|
295
422
|
// built-in local commands exist.
|
|
296
423
|
const commandService = ctx.get('commands');
|
|
424
|
+
// messages.observe broker (optional service, C-042): mounted by the
|
|
425
|
+
// dsh-tui-plugin-host row; absent the row, publish is a no-op and nothing
|
|
426
|
+
// else changes (soft degradation, #183).
|
|
427
|
+
const messageObserver = getHostMessageObserver(ctx.get('tuiMessageObserver'));
|
|
297
428
|
// Workspace registry runtime (optional service, issue #183): mounted by
|
|
298
429
|
// the bundle patch's dsh-tui-workspaces row; absent the row (stale patch
|
|
299
430
|
// or a bare embedder), degrade to the local-only runtime. plugin.ts owns
|
|
300
431
|
// the degraded-boot warning for profile launches.
|
|
301
|
-
const workspaceService = ctx.get('tuiWorkspaces') ?? createLocalWorkspaceRuntime();
|
|
302
|
-
const commandTrees = ctx.get('tuiCommandTrees');
|
|
432
|
+
const workspaceService = getHostWorkspaceRuntime(ctx.get('tuiWorkspaces')) ?? createLocalWorkspaceRuntime();
|
|
433
|
+
const commandTrees = getHostCommandTrees(ctx.get('tuiCommandTrees'));
|
|
303
434
|
// The `/settings` screen reads its host on EVERY render, so the host must
|
|
304
435
|
// be a stable object: a fresh literal per call would re-fire the screen's
|
|
305
436
|
// host-keyed effects endlessly (render → new host → effect → state →
|
|
@@ -310,7 +441,12 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
310
441
|
// Plugin scene runtime (optional service, same degradation rule as
|
|
311
442
|
// tuiWorkspaces/tuiCommandTrees): mounted by the bundle patch's
|
|
312
443
|
// dsh-tui-scenes row; absent the row, `pluginScene` simply stays undefined.
|
|
313
|
-
const sceneRuntime = ctx.get('tuiScenes');
|
|
444
|
+
const sceneRuntime = getHostSceneRuntime(ctx.get('tuiScenes'));
|
|
445
|
+
const settingsSectionsRuntime = getHostSettingsSections(ctx.get('tuiSettingsSections'));
|
|
446
|
+
// Custom-entry text renderers (optional service, dsh-tui-extensions row):
|
|
447
|
+
// absent the row, unknown plugin event types stay invisible in the
|
|
448
|
+
// transcript, exactly as before the seam existed.
|
|
449
|
+
const rendererRuntime = getHostRenderers(ctx.get('tuiRenderers'));
|
|
314
450
|
// Shift+Tab session-mode cycle: cordis.yml `modes` wins; absent/empty/
|
|
315
451
|
// atom-less → the built-in default/plan/full cycle (sessionModes.ts).
|
|
316
452
|
const { modes: sessionModes, dropped: droppedModeIds } = resolveSessionModes(options.modes);
|
|
@@ -320,6 +456,9 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
320
456
|
const listeners = new Set();
|
|
321
457
|
/** True while a frame-aligned stream notification is pending (emitStream). */
|
|
322
458
|
let streamNotifyScheduled = false;
|
|
459
|
+
// foldRows incremental cursor (see foldRows): rows only append past the
|
|
460
|
+
// fold line, so each pass touches only newly-eligible rows.
|
|
461
|
+
const foldCursor = { rows: null, index: 0 };
|
|
323
462
|
let nextNotificationId = 1;
|
|
324
463
|
/** One-shot context-low warning per session (CC's TokenWarning). */
|
|
325
464
|
let contextWarned = false;
|
|
@@ -405,6 +544,142 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
405
544
|
state.notify(t('send-failed', { err: message }), { color: 'error' });
|
|
406
545
|
});
|
|
407
546
|
};
|
|
547
|
+
/**
|
|
548
|
+
* RFC 0005 D-8: a flow parked on a plugin decision must be user-observable.
|
|
549
|
+
* Decisions normally resolve in milliseconds, so the notice only fires
|
|
550
|
+
* once the wait crosses a threshold — a slow plugin (e.g. one showing a
|
|
551
|
+
* managed dialog) then explains the pause instead of looking like the TUI
|
|
552
|
+
* ate the input.
|
|
553
|
+
*/
|
|
554
|
+
const DECISION_PENDING_MS = 400;
|
|
555
|
+
const withDecisionPending = (name, pending) => {
|
|
556
|
+
let dismiss;
|
|
557
|
+
const timer = setTimeout(() => {
|
|
558
|
+
// Sticky (timeoutMs 0), D-8: the indicator must cover the WHOLE wait —
|
|
559
|
+
// an auto-expiring notice would vanish after ~4s while the decision,
|
|
560
|
+
// the delivery and every queued FIFO task behind them stay parked,
|
|
561
|
+
// leaving the user with no sign the flow is still waiting. It comes
|
|
562
|
+
// down only when the decision settles (finally below); a decision
|
|
563
|
+
// that never settles keeps its indicator up, which is the truthful
|
|
564
|
+
// state.
|
|
565
|
+
dismiss = state.notify(t('ext-decision-pending', { event: name }), { timeoutMs: 0 });
|
|
566
|
+
}, DECISION_PENDING_MS);
|
|
567
|
+
// Both exits are covered: a fast decision clears the timer before it
|
|
568
|
+
// fires; a slow one dismisses the indicator it raised.
|
|
569
|
+
return pending.finally(() => {
|
|
570
|
+
clearTimeout(timer);
|
|
571
|
+
dismiss?.();
|
|
572
|
+
});
|
|
573
|
+
};
|
|
574
|
+
/**
|
|
575
|
+
* The `tui/input` decision event (pi's `input` seam): the FIRST plugin
|
|
576
|
+
* returning a valid decision wins — transform the text, mark it handled,
|
|
577
|
+
* or cancel it. No listeners (or only crashing/malformed ones) means
|
|
578
|
+
* delivery proceeds unchanged, so a broken plugin can never wedge the
|
|
579
|
+
* input path — and can never skip a later veto listener either
|
|
580
|
+
* (dispatchTuiDecision isolates crashes and normalizes returns per
|
|
581
|
+
* listener instead of bailing on the first object).
|
|
582
|
+
*
|
|
583
|
+
* Decision AND delivery enter one FIFO chain in submission order: a slow
|
|
584
|
+
* listener on A parks A's delivery AND any later submissions behind it —
|
|
585
|
+
* without the chain, B's decision could resolve first and the model would
|
|
586
|
+
* receive B before A. Each submission binds its origin agent AT ENQUEUE,
|
|
587
|
+
* so a session switch landing before OR during its decision drops the
|
|
588
|
+
* stale text with a notice instead of sending the old conversation's
|
|
589
|
+
* words to the new session.
|
|
590
|
+
*/
|
|
591
|
+
let inputChain = Promise.resolve();
|
|
592
|
+
const runUserTextDecision = async (text, placement, originAgent, originAgentId) => {
|
|
593
|
+
// Stale detection compares the AGENT REFERENCE, not the id: session ids
|
|
594
|
+
// are reusable (A → /new → /resume A lands back on the same id with a
|
|
595
|
+
// fresh agent), so an id check has an ABA hole. Both origin values are
|
|
596
|
+
// ENQUEUE-time captures (see dispatchUserText): a decision parked behind
|
|
597
|
+
// a slow predecessor must still be judged against the session its text
|
|
598
|
+
// was typed in, not whichever session is live when it finally runs.
|
|
599
|
+
const decision = await withDecisionPending('tui/input', dispatchTuiDecision(ctx, 'tui/input', {
|
|
600
|
+
text,
|
|
601
|
+
delivery: placement === 'steer' ? 'steer' : 'followup',
|
|
602
|
+
sessionId: originAgentId,
|
|
603
|
+
cwd: state.cwd,
|
|
604
|
+
}, normalizeInputDecision));
|
|
605
|
+
if (decision !== undefined) {
|
|
606
|
+
// Both intercepts toast — a bare {cancel}/{handled} must not make the
|
|
607
|
+
// typed line vanish silently (the host-localized fallback mirrors the
|
|
608
|
+
// other decision events' ext-action-cancelled handling).
|
|
609
|
+
if ('cancel' in decision) {
|
|
610
|
+
state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if ('handled' in decision) {
|
|
614
|
+
state.notify(decision.notice ?? t('ext-action-handled'), { timeoutMs: 4000 });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
text = decision.text.trim();
|
|
618
|
+
}
|
|
619
|
+
if (agent !== originAgent) {
|
|
620
|
+
state.notify(t('ext-stale-dropped'), { color: 'warning', timeoutMs: 4000 });
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
deliverUserText(text, placement);
|
|
624
|
+
};
|
|
625
|
+
const dispatchUserText = (text, placement) => {
|
|
626
|
+
// D-6: bind the submission to the session it was typed in AT ENQUEUE
|
|
627
|
+
// TIME. The FIFO chain may park this task behind a slow predecessor
|
|
628
|
+
// while the user /new's away — capturing the agent at run time would
|
|
629
|
+
// adopt the NEW session as this text's origin and deliver the old
|
|
630
|
+
// conversation's words into it.
|
|
631
|
+
const originAgent = agent;
|
|
632
|
+
const originAgentId = state.agentId;
|
|
633
|
+
inputChain = inputChain.then(() => runUserTextDecision(text, placement, originAgent, originAgentId)).catch((error) => {
|
|
634
|
+
// The chain must survive a failed decision: log, then continue with
|
|
635
|
+
// the next queued submission.
|
|
636
|
+
ctx.logger.warn('dsh-tui: tui/input dispatch failed: %o', error);
|
|
637
|
+
});
|
|
638
|
+
};
|
|
639
|
+
/**
|
|
640
|
+
* The `tui/session-switch` decision event (pi's `session_before_switch`),
|
|
641
|
+
* fired before `/new` or `/resume` replaces the live session (rewind has
|
|
642
|
+
* its own prompt event). The first answering plugin may veto the switch;
|
|
643
|
+
* the reason is toasted here so the fallback string stays host-localized.
|
|
644
|
+
*/
|
|
645
|
+
const sessionSwitchVetoed = async (kind, targetSessionId) => {
|
|
646
|
+
// D-6 stale detection captures the AGENT REFERENCE (session ids are
|
|
647
|
+
// reusable — ABA): a slow decision must not let an older /resume roll
|
|
648
|
+
// over a newer session the user already switched to mid-await.
|
|
649
|
+
const originAgent = agent;
|
|
650
|
+
const decision = await withDecisionPending('tui/session-switch', dispatchTuiDecision(ctx, 'tui/session-switch', {
|
|
651
|
+
kind,
|
|
652
|
+
...(targetSessionId === undefined ? {} : { targetSessionId }),
|
|
653
|
+
sessionId: state.agentId,
|
|
654
|
+
cwd: state.cwd,
|
|
655
|
+
}, normalizeCancelDecision));
|
|
656
|
+
if (agent !== originAgent) {
|
|
657
|
+
// The world changed while the decision parked: drop the pending
|
|
658
|
+
// switch instead of replacing the user's newer session.
|
|
659
|
+
state.notify(t('ext-stale-dropped'), { color: 'warning', timeoutMs: 4000 });
|
|
660
|
+
return true;
|
|
661
|
+
}
|
|
662
|
+
if (decision !== undefined) {
|
|
663
|
+
state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
return false;
|
|
667
|
+
};
|
|
668
|
+
/** Fire-and-forget `tui/session-switched` (parallel): per-session plugin
|
|
669
|
+
* state rebinds here. Listener failures are logged, never propagated —
|
|
670
|
+
* the switch itself already succeeded. */
|
|
671
|
+
const notifySessionSwitched = (kind, sessionId, previousSessionId) => {
|
|
672
|
+
try {
|
|
673
|
+
void dispatchTuiNotification(ctx, 'tui/session-switched', { kind, sessionId, previousSessionId, cwd: state.cwd }).catch((error) => {
|
|
674
|
+
ctx.logger.warn('dsh-tui: tui/session-switched listener failed: %o', error);
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
// A bare embedder's context may lack the event bus entirely; the
|
|
679
|
+
// switch itself already succeeded, so this stays a log line.
|
|
680
|
+
ctx.logger.warn('dsh-tui: tui/session-switched dispatch failed: %o', error);
|
|
681
|
+
}
|
|
682
|
+
};
|
|
408
683
|
/** Monotonic token: only the latest `interruptAndDeliver` re-queues, so a
|
|
409
684
|
* second interrupt while the abort settles cannot double-deliver. */
|
|
410
685
|
let interruptSeq = 0;
|
|
@@ -519,6 +794,45 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
519
794
|
const executeRegistryCommand = async (name, rawInput) => {
|
|
520
795
|
if (!commandService)
|
|
521
796
|
return undefined;
|
|
797
|
+
// Resolve the exact definition that execute() will select for this agent.
|
|
798
|
+
// Same names may exist in distinct agent scopes, so a name-only lookup can
|
|
799
|
+
// apply another scope's owner policy.
|
|
800
|
+
const definition = commandService.find(agent, name);
|
|
801
|
+
const owner = commandOwner(ctx, definition);
|
|
802
|
+
// The root checkpoint covers host/direct registrations. A built-in name
|
|
803
|
+
// is not necessarily a namespaced contribution id, so use a deterministic
|
|
804
|
+
// host scope for that case; legacy unscoped denies still conservatively
|
|
805
|
+
// revoke every valid command scope.
|
|
806
|
+
const rootScope = owner?.commandId
|
|
807
|
+
?? (/^[a-z][a-z0-9]*(?:[.-][a-z0-9][a-z0-9-]*)+$/u.test(name)
|
|
808
|
+
? name
|
|
809
|
+
: `dsh-tui.${name.toLowerCase().replace(/[^a-z0-9-]+/gu, '-').replace(/^-+|-+$/gu, '') || 'command'}`);
|
|
810
|
+
if (!currentGrantStore().allows({ componentId: 'root' }, 'commands.invoke', rootScope)) {
|
|
811
|
+
ctx.logger.warn('dsh-tui: registry command invocation denied (commands.invoke revoked for "root" in the grants file)');
|
|
812
|
+
ctx.get('tuiEffectLedger')?.record({
|
|
813
|
+
operation: 'bind',
|
|
814
|
+
resource: { kind: 'permission', id: `root:commands.invoke:${rootScope}` },
|
|
815
|
+
result: 'failed',
|
|
816
|
+
errorCode: 'PERMISSION_NOT_GRANTED',
|
|
817
|
+
}, ctx);
|
|
818
|
+
return t('command-invoke-denied');
|
|
819
|
+
}
|
|
820
|
+
// Per-owner gate (C-041): a command REGISTERED BY A PLUGIN through the
|
|
821
|
+
// plugin-host row's mediated registerCommand (see command-attribution.js)
|
|
822
|
+
// is additionally gated on the OWNER's grant, so a denies entry for the
|
|
823
|
+
// plugin closes the host-mediated invocation of ITS commands.
|
|
824
|
+
// Unattributed host/direct registrations remain inside the documented
|
|
825
|
+
// trusted-in-process boundary and have no plugin grant to evaluate.
|
|
826
|
+
if (owner !== undefined && !currentGrantStore().allows({ componentId: owner.componentId, activationId: owner.activationId }, 'commands.invoke', owner.commandId)) {
|
|
827
|
+
ctx.logger.warn(`dsh-tui: registry command "/${name}" invocation denied — owner Component "${owner.componentId}" lost commands.invoke for "${owner.commandId}"`);
|
|
828
|
+
ctx.get('tuiEffectLedger')?.record({
|
|
829
|
+
operation: 'bind',
|
|
830
|
+
resource: { kind: 'permission', id: `${owner.componentId}:commands.invoke:${owner.commandId}` },
|
|
831
|
+
result: 'failed',
|
|
832
|
+
errorCode: 'PERMISSION_NOT_GRANTED',
|
|
833
|
+
}, ctx);
|
|
834
|
+
return t('command-invoke-denied-owner', { name, owner: owner.componentId });
|
|
835
|
+
}
|
|
522
836
|
try {
|
|
523
837
|
const execution = await commandService.execute(agent, `/${name}${rawInput}`, new AbortController().signal);
|
|
524
838
|
// `undefined` = not registered; a handler error surfaces as its
|
|
@@ -653,6 +967,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
653
967
|
workingActivity: undefined,
|
|
654
968
|
activityFrames: options.activityFrames,
|
|
655
969
|
diffLayout: options.diffLayout ?? 'auto',
|
|
970
|
+
thinkingFold: options.thinkingFold ?? 'preview',
|
|
656
971
|
activityEnabled: options.activity !== false,
|
|
657
972
|
contextBarEnabled: options.contextBar !== false,
|
|
658
973
|
agentPreset: options.agentPreset,
|
|
@@ -701,7 +1016,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
701
1016
|
};
|
|
702
1017
|
},
|
|
703
1018
|
emit() {
|
|
704
|
-
foldRows(state.rows, MAX_ROWS);
|
|
1019
|
+
foldRows(state.rows, MAX_ROWS, foldCursor);
|
|
705
1020
|
state.version += 1;
|
|
706
1021
|
for (const listener of listeners)
|
|
707
1022
|
listener();
|
|
@@ -719,7 +1034,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
719
1034
|
streamNotifyScheduled = true;
|
|
720
1035
|
const timer = setTimeout(() => {
|
|
721
1036
|
streamNotifyScheduled = false;
|
|
722
|
-
foldRows(state.rows, MAX_ROWS);
|
|
1037
|
+
foldRows(state.rows, MAX_ROWS, foldCursor);
|
|
723
1038
|
for (const listener of listeners)
|
|
724
1039
|
listener();
|
|
725
1040
|
}, 16);
|
|
@@ -778,7 +1093,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
778
1093
|
// The current session is being used — move it to the MRU front
|
|
779
1094
|
// (/resume sorts by last-used).
|
|
780
1095
|
touchSession(state.agentId);
|
|
781
|
-
|
|
1096
|
+
void dispatchUserText(trimmed, 'followup');
|
|
782
1097
|
},
|
|
783
1098
|
/** Steer a message into the RUNNING turn (Codex/pi semantics): it is
|
|
784
1099
|
* injected at the next step boundary of the current turn and the agent
|
|
@@ -788,11 +1103,13 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
788
1103
|
if (!trimmed)
|
|
789
1104
|
return;
|
|
790
1105
|
touchSession(state.agentId);
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
|
|
1106
|
+
// Same tui/input decision pass as submit; the delivery re-validates
|
|
1107
|
+
// the live agent after the await. Official dsh-agent rc.6: steer() is
|
|
1108
|
+
// synchronous void — the message enters the next-step inbox; a
|
|
1109
|
+
// rejected step leaves it parked for the next wake, and the inbox
|
|
1110
|
+
// events retire the preview (claimed → turn boundary, discarded →
|
|
1111
|
+
// cancel).
|
|
1112
|
+
void dispatchUserText(trimmed, 'steer');
|
|
796
1113
|
},
|
|
797
1114
|
/** Pull a pending message back out of the inbox (Alt+Up): it returns to
|
|
798
1115
|
* the input for editing instead of being delivered. */
|
|
@@ -838,7 +1155,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
838
1155
|
return;
|
|
839
1156
|
for (const text of queued) {
|
|
840
1157
|
touchSession(state.agentId);
|
|
841
|
-
|
|
1158
|
+
// Same tui/input decision pass as a typed submit: Ctrl+Enter must
|
|
1159
|
+
// not bypass a plugin's cancel/transform policy, and re-queued
|
|
1160
|
+
// texts keep submission order through the one FIFO chain.
|
|
1161
|
+
dispatchUserText(text, 'followup');
|
|
842
1162
|
}
|
|
843
1163
|
};
|
|
844
1164
|
if (typeof whenIdle === 'function') {
|
|
@@ -851,7 +1171,43 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
851
1171
|
}
|
|
852
1172
|
return queued.length;
|
|
853
1173
|
},
|
|
854
|
-
|
|
1174
|
+
/**
|
|
1175
|
+
* The `tui/rewind-prompt` decision event (pi's `session_before_fork`):
|
|
1176
|
+
* fired when the rewind picker confirms a message, before any fork
|
|
1177
|
+
* work. The first answering plugin may cancel the rewind (the picker
|
|
1178
|
+
* stays open; the reason is toasted here so the UI string stays
|
|
1179
|
+
* host-localized when absent) or offer extra modes rendered in the
|
|
1180
|
+
* confirm pane. Returns 'cancel', the modes, or null for "no opinion".
|
|
1181
|
+
*/
|
|
1182
|
+
async promptRewind(row) {
|
|
1183
|
+
if (row.seq === undefined)
|
|
1184
|
+
return null;
|
|
1185
|
+
// D-6, same as the other decision points: a slow /new or /resume can
|
|
1186
|
+
// replace the agent while this decision parks. Without the identity
|
|
1187
|
+
// check the picker would go on to show the OLD session's row in the
|
|
1188
|
+
// confirm pane and rewindTo would cut the NEW session at the old
|
|
1189
|
+
// seq — a wrong rewind or a fork failure. Compare agent REFERENCES
|
|
1190
|
+
// (session ids are reusable — ABA) and stale-cancel.
|
|
1191
|
+
const originAgent = agent;
|
|
1192
|
+
const decision = await withDecisionPending('tui/rewind-prompt', dispatchTuiDecision(ctx, 'tui/rewind-prompt', {
|
|
1193
|
+
text: row.text,
|
|
1194
|
+
seq: row.seq,
|
|
1195
|
+
sessionId: state.agentId,
|
|
1196
|
+
cwd: state.cwd,
|
|
1197
|
+
}, normalizeRewindPromptDecision));
|
|
1198
|
+
if (agent !== originAgent) {
|
|
1199
|
+
state.notify(t('ext-stale-dropped'), { color: 'warning', timeoutMs: 4000 });
|
|
1200
|
+
return 'cancel';
|
|
1201
|
+
}
|
|
1202
|
+
if (decision === undefined)
|
|
1203
|
+
return null;
|
|
1204
|
+
if ('cancel' in decision) {
|
|
1205
|
+
state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
|
|
1206
|
+
return 'cancel';
|
|
1207
|
+
}
|
|
1208
|
+
return { modes: decision.modes };
|
|
1209
|
+
},
|
|
1210
|
+
async rewindTo(row, mode = null) {
|
|
855
1211
|
if (row.seq === undefined)
|
|
856
1212
|
return null;
|
|
857
1213
|
const sessions = ctx.get('sessions');
|
|
@@ -947,6 +1303,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
947
1303
|
// counters land back at the rewind point, matching the fork).
|
|
948
1304
|
streaming = undefined;
|
|
949
1305
|
reasoning = undefined;
|
|
1306
|
+
// Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
|
|
1307
|
+
// keep it out of the next turn's settle logs and revive cache.
|
|
1308
|
+
sealedReasoning.length = 0;
|
|
1309
|
+
lastReasoningRow = undefined;
|
|
950
1310
|
toolCards.clear();
|
|
951
1311
|
nextRowId = 0;
|
|
952
1312
|
state.rows.length = 0;
|
|
@@ -980,10 +1340,16 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
980
1340
|
thinking: 0,
|
|
981
1341
|
tools: 0,
|
|
982
1342
|
};
|
|
983
|
-
|
|
984
|
-
|
|
1343
|
+
replayEvents(seed);
|
|
1344
|
+
settleStreaming();
|
|
1345
|
+
// A seed ending mid-turn replays a turn/start that set working=true;
|
|
1346
|
+
// the boot path resets this after replay — mirror it here so an idle
|
|
1347
|
+
// rewound agent doesn't sit with a live spinner (a still-running
|
|
1348
|
+
// agent re-asserts on its next event).
|
|
1349
|
+
state.working = handle.agent.status === 'running';
|
|
985
1350
|
// Rebind subscriptions to the new agent, then free the old one.
|
|
986
1351
|
const oldHandle = currentHandle;
|
|
1352
|
+
const sourceSessionId = String(agent.session.id);
|
|
987
1353
|
agent = handle.agent;
|
|
988
1354
|
currentHandle = handle;
|
|
989
1355
|
bindAgent();
|
|
@@ -994,6 +1360,43 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
994
1360
|
touchSession(childId);
|
|
995
1361
|
state.emit();
|
|
996
1362
|
void oldHandle?.dispose().catch(() => { });
|
|
1363
|
+
// Decision-event pair around the completed rewind: `tui/rewind-done`
|
|
1364
|
+
// (the first non-empty string is toasted as the post-rewind summary,
|
|
1365
|
+
// e.g. a plugin reporting restored files) and the generic
|
|
1366
|
+
// `tui/session-switched` notification. Listener failures are logged,
|
|
1367
|
+
// never surfaced — the rewind itself already succeeded.
|
|
1368
|
+
//
|
|
1369
|
+
// rewind-done is a post-hoc summary, NOT a gate, so it is dispatched
|
|
1370
|
+
// DECOUPLED from the return value: the picker is already closed and
|
|
1371
|
+
// PromptInput is live — awaiting a slow listener here would delay the
|
|
1372
|
+
// picked text's return to the draft, letting its late arrival
|
|
1373
|
+
// overwrite whatever the user typed meanwhile, and a listener that
|
|
1374
|
+
// never settles would park tui/session-switched forever. The summary
|
|
1375
|
+
// toasts whenever it lands.
|
|
1376
|
+
try {
|
|
1377
|
+
void dispatchTuiDecision(ctx, 'tui/rewind-done', {
|
|
1378
|
+
text: row.text,
|
|
1379
|
+
mode,
|
|
1380
|
+
boundarySeq: boundary,
|
|
1381
|
+
sourceSessionId,
|
|
1382
|
+
childSessionId: String(childId),
|
|
1383
|
+
sessionId: String(childId),
|
|
1384
|
+
cwd: state.cwd,
|
|
1385
|
+
}, normalizeRewindDoneSummary)
|
|
1386
|
+
.then(summary => {
|
|
1387
|
+
if (summary !== undefined)
|
|
1388
|
+
state.notify(summary, { timeoutMs: 6000 });
|
|
1389
|
+
})
|
|
1390
|
+
.catch((error) => {
|
|
1391
|
+
ctx.logger.warn('dsh-tui: tui/rewind-done dispatch failed: %o', error);
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
catch (error) {
|
|
1395
|
+
// A bare embedder's context may lack the event bus entirely; the
|
|
1396
|
+
// rewind itself already succeeded, so this stays a log line.
|
|
1397
|
+
ctx.logger.warn('dsh-tui: tui/rewind-done dispatch failed: %o', error);
|
|
1398
|
+
}
|
|
1399
|
+
notifySessionSwitched('rewind', String(childId), sourceSessionId);
|
|
997
1400
|
return row.text;
|
|
998
1401
|
},
|
|
999
1402
|
async resumeTo(sessionId) {
|
|
@@ -1002,13 +1405,18 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1002
1405
|
// resolving through DSH_TUI_RESUME_SESSION at boot).
|
|
1003
1406
|
if (state.working) {
|
|
1004
1407
|
state.notify(t('resume-while-working'), { color: 'warning' });
|
|
1005
|
-
return false;
|
|
1408
|
+
return { ok: false, reason: 'working' };
|
|
1006
1409
|
}
|
|
1007
1410
|
const agents = ctx.get('agents');
|
|
1008
1411
|
if (!agents) {
|
|
1009
1412
|
state.notify(t('resume-unavailable'), { color: 'error' });
|
|
1010
|
-
return false;
|
|
1413
|
+
return { ok: false, reason: 'unavailable' };
|
|
1011
1414
|
}
|
|
1415
|
+
// Plugin veto point (tui/session-switch): before any read of the
|
|
1416
|
+
// target — a veto leaves the live session and its transcript
|
|
1417
|
+
// untouched.
|
|
1418
|
+
if (await sessionSwitchVetoed('resume', sessionId))
|
|
1419
|
+
return { ok: false, reason: 'cancelled' };
|
|
1012
1420
|
let handle;
|
|
1013
1421
|
// Compat boundary: register vouched-for legacy event types (e.g.
|
|
1014
1422
|
// activity/status from pre-#143 logs) in every reachable dsh-session
|
|
@@ -1039,7 +1447,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1039
1447
|
catch (error) {
|
|
1040
1448
|
const message = error instanceof Error ? error.message : String(error);
|
|
1041
1449
|
state.notify(t('resume-failed', { err: message }), { color: 'error', timeoutMs: 8000 });
|
|
1042
|
-
return false;
|
|
1450
|
+
return { ok: false, reason: 'failed', error: message };
|
|
1043
1451
|
}
|
|
1044
1452
|
try {
|
|
1045
1453
|
// `/resume` is an explicit adoption of this persisted conversation.
|
|
@@ -1054,6 +1462,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1054
1462
|
// rewindTo, plus the context window which the replay re-derives).
|
|
1055
1463
|
streaming = undefined;
|
|
1056
1464
|
reasoning = undefined;
|
|
1465
|
+
// Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
|
|
1466
|
+
// keep it out of the next turn's settle logs and revive cache.
|
|
1467
|
+
sealedReasoning.length = 0;
|
|
1468
|
+
lastReasoningRow = undefined;
|
|
1057
1469
|
toolCards.clear();
|
|
1058
1470
|
nextRowId = 0;
|
|
1059
1471
|
state.rows.length = 0;
|
|
@@ -1106,11 +1518,15 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1106
1518
|
thinking: 0,
|
|
1107
1519
|
tools: 0,
|
|
1108
1520
|
};
|
|
1109
|
-
|
|
1110
|
-
renderEvent(event);
|
|
1521
|
+
replayEvents(handle.agent.session.events);
|
|
1111
1522
|
settleStreaming();
|
|
1523
|
+
// A log ending mid-turn replays a turn/start that set working=true;
|
|
1524
|
+
// mirror the boot path's post-replay reset (a still-running agent
|
|
1525
|
+
// re-asserts on its next event).
|
|
1526
|
+
state.working = handle.agent.status === 'running';
|
|
1112
1527
|
// Rebind subscriptions to the resumed agent, then free the old one.
|
|
1113
1528
|
const oldHandle = currentHandle;
|
|
1529
|
+
const previousSessionId = String(agent.session.id);
|
|
1114
1530
|
agent = handle.agent;
|
|
1115
1531
|
currentHandle = handle;
|
|
1116
1532
|
bindAgent();
|
|
@@ -1124,7 +1540,8 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1124
1540
|
state.emit();
|
|
1125
1541
|
void oldHandle?.dispose().catch(() => { });
|
|
1126
1542
|
clearStagedImages();
|
|
1127
|
-
|
|
1543
|
+
notifySessionSwitched('resume', sessionId, previousSessionId);
|
|
1544
|
+
return { ok: true };
|
|
1128
1545
|
},
|
|
1129
1546
|
async newSession() {
|
|
1130
1547
|
// `/new` — start a fresh conversation: brand-new agent + session, the
|
|
@@ -1143,6 +1560,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1143
1560
|
});
|
|
1144
1561
|
return false;
|
|
1145
1562
|
}
|
|
1563
|
+
// Plugin veto point (tui/session-switch): no side effects have
|
|
1564
|
+
// happened yet — the session id below is not even allocated.
|
|
1565
|
+
if (await sessionSwitchVetoed('new'))
|
|
1566
|
+
return false;
|
|
1146
1567
|
const sessionId = SessionId(randomUUID());
|
|
1147
1568
|
let handle;
|
|
1148
1569
|
// A fresh session composes the caller's DEFAULT preset: the cordis.yml
|
|
@@ -1197,6 +1618,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1197
1618
|
}
|
|
1198
1619
|
streaming = undefined;
|
|
1199
1620
|
reasoning = undefined;
|
|
1621
|
+
// Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
|
|
1622
|
+
// keep it out of the next turn's settle logs and revive cache.
|
|
1623
|
+
sealedReasoning.length = 0;
|
|
1624
|
+
lastReasoningRow = undefined;
|
|
1200
1625
|
toolCards.clear();
|
|
1201
1626
|
nextRowId = 0;
|
|
1202
1627
|
state.rows.length = 0;
|
|
@@ -1234,6 +1659,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1234
1659
|
tools: 0,
|
|
1235
1660
|
};
|
|
1236
1661
|
const oldHandle = currentHandle;
|
|
1662
|
+
const previousSessionId = String(agent.session.id);
|
|
1237
1663
|
agent = handle.agent;
|
|
1238
1664
|
currentHandle = handle;
|
|
1239
1665
|
bindAgent();
|
|
@@ -1245,6 +1671,7 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1245
1671
|
touchSession(handle.agent.id);
|
|
1246
1672
|
void oldHandle?.dispose().catch(() => { });
|
|
1247
1673
|
clearStagedImages();
|
|
1674
|
+
notifySessionSwitched('new', String(handle.agent.id), previousSessionId);
|
|
1248
1675
|
return true;
|
|
1249
1676
|
},
|
|
1250
1677
|
listWorkspaces() {
|
|
@@ -1368,6 +1795,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1368
1795
|
}
|
|
1369
1796
|
streaming = undefined;
|
|
1370
1797
|
reasoning = undefined;
|
|
1798
|
+
// Stale sealed/thinking bookkeeping belongs to the OLD agent's rows;
|
|
1799
|
+
// keep it out of the next turn's settle logs and revive cache.
|
|
1800
|
+
sealedReasoning.length = 0;
|
|
1801
|
+
lastReasoningRow = undefined;
|
|
1371
1802
|
toolCards.clear();
|
|
1372
1803
|
nextRowId = 0;
|
|
1373
1804
|
state.rows.length = 0;
|
|
@@ -1404,9 +1835,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1404
1835
|
thinking: 0,
|
|
1405
1836
|
tools: 0,
|
|
1406
1837
|
};
|
|
1407
|
-
|
|
1408
|
-
renderEvent(event);
|
|
1838
|
+
replayEvents(seed);
|
|
1409
1839
|
settleStreaming();
|
|
1840
|
+
// Same mid-turn-seed spinner reset as resume above.
|
|
1841
|
+
state.working = handle.agent.status === 'running';
|
|
1410
1842
|
const oldHandle = currentHandle;
|
|
1411
1843
|
agent = handle.agent;
|
|
1412
1844
|
currentHandle = handle;
|
|
@@ -1456,13 +1888,24 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1456
1888
|
};
|
|
1457
1889
|
state.notifications.push(item);
|
|
1458
1890
|
state.emit();
|
|
1459
|
-
|
|
1891
|
+
const remove = () => {
|
|
1460
1892
|
const index = state.notifications.indexOf(item);
|
|
1461
1893
|
if (index >= 0) {
|
|
1462
1894
|
state.notifications.splice(index, 1);
|
|
1463
1895
|
state.emit();
|
|
1464
1896
|
}
|
|
1465
|
-
}
|
|
1897
|
+
};
|
|
1898
|
+
// timeoutMs 0 = sticky: no expiry timer, the dismiss handle is the
|
|
1899
|
+
// only way out (a decision-parked indicator must outlive the wait it
|
|
1900
|
+
// describes — auto-expiring it would hide a still-parked flow, D-8).
|
|
1901
|
+
const expire = item.timeoutMs > 0 ? setTimeout(remove, item.timeoutMs) : undefined;
|
|
1902
|
+
// Early-dismiss handle for flows that know their notice went stale
|
|
1903
|
+
// (e.g. a decision-parked indicator whose decision just landed).
|
|
1904
|
+
return () => {
|
|
1905
|
+
if (expire !== undefined)
|
|
1906
|
+
clearTimeout(expire);
|
|
1907
|
+
remove();
|
|
1908
|
+
};
|
|
1466
1909
|
},
|
|
1467
1910
|
setDiffLayout(layout) {
|
|
1468
1911
|
if (layout === state.diffLayout)
|
|
@@ -1470,6 +1913,12 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1470
1913
|
state.diffLayout = layout;
|
|
1471
1914
|
state.emit();
|
|
1472
1915
|
},
|
|
1916
|
+
setThinkingFold(mode) {
|
|
1917
|
+
if (mode === state.thinkingFold)
|
|
1918
|
+
return;
|
|
1919
|
+
state.thinkingFold = mode;
|
|
1920
|
+
state.emit();
|
|
1921
|
+
},
|
|
1473
1922
|
setActivityFrames(name) {
|
|
1474
1923
|
if (!isPresetName(name)) {
|
|
1475
1924
|
state.notify(t('unknown-activity-preset', { name }), { color: 'error' });
|
|
@@ -1650,12 +2099,10 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1650
2099
|
return settingsHostCache;
|
|
1651
2100
|
},
|
|
1652
2101
|
settingsSections() {
|
|
1653
|
-
|
|
1654
|
-
return sections?.list() ?? [];
|
|
2102
|
+
return settingsSectionsRuntime?.list() ?? [];
|
|
1655
2103
|
},
|
|
1656
2104
|
subscribeSettingsSections(listener) {
|
|
1657
|
-
|
|
1658
|
-
return sections?.subscribe(listener) ?? (() => { });
|
|
2105
|
+
return settingsSectionsRuntime?.subscribe(listener) ?? (() => { });
|
|
1659
2106
|
},
|
|
1660
2107
|
providerSetup() {
|
|
1661
2108
|
// The `/provider` wizard's runtime surface, over the dsh-base seams:
|
|
@@ -1839,16 +2286,49 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
1839
2286
|
state.notify(t('compact-while-working'), { color: 'warning' });
|
|
1840
2287
|
return;
|
|
1841
2288
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
2289
|
+
// Plugin veto point (tui/compact): the first answering plugin may
|
|
2290
|
+
// cancel the compaction before anything runs.
|
|
2291
|
+
const originAgentId = state.agentId;
|
|
2292
|
+
// Compare the AGENT REFERENCE after the await, not the id — session
|
|
2293
|
+
// ids are reusable (A → /new → /resume A returns the same id on a new
|
|
2294
|
+
// agent), so an id comparison has an ABA hole that would hand the new
|
|
2295
|
+
// agent to the OLD scope's compaction service.
|
|
2296
|
+
const originAgent = agent;
|
|
2297
|
+
void (async () => {
|
|
2298
|
+
const decision = await withDecisionPending('tui/compact', dispatchTuiDecision(ctx, 'tui/compact', {
|
|
2299
|
+
sessionId: originAgentId,
|
|
2300
|
+
cwd: state.cwd,
|
|
2301
|
+
}, normalizeCancelDecision));
|
|
2302
|
+
if (decision !== undefined) {
|
|
2303
|
+
state.notify(decision.reason ?? t('ext-action-cancelled'), { color: 'warning', timeoutMs: 4000 });
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
// Stale-drop (same rule as tui/input): the await parked us while the
|
|
2307
|
+
// user switched sessions — `compactService` was resolved through the
|
|
2308
|
+
// OLD agent's scope chain, and the mutable `agent` now points at the
|
|
2309
|
+
// new session. Running now would hand the new agent to the old
|
|
2310
|
+
// service (or call into an unloaded one).
|
|
2311
|
+
if (agent !== originAgent) {
|
|
2312
|
+
state.notify(t('ext-compact-stale'), { color: 'warning', timeoutMs: 4000 });
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
if (state.working) {
|
|
2316
|
+
// The await above gave a queued turn time to start; compacting
|
|
2317
|
+
// mid-turn now would be the same race the check upfront avoided.
|
|
2318
|
+
state.notify(t('compact-while-working'), { color: 'warning' });
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
const signal = new AbortController().signal;
|
|
2322
|
+
state.notify(t('compact-working'));
|
|
2323
|
+
void compactService
|
|
2324
|
+
.compactNow(agent, signal)
|
|
2325
|
+
.then((result) => {
|
|
2326
|
+
state.notify(result ? t('compact-done') : t('compact-nothing'));
|
|
2327
|
+
})
|
|
2328
|
+
.catch((error) => {
|
|
2329
|
+
state.notify(t('compact-failed', { err: error instanceof Error ? error.message : String(error) }), { color: 'error', timeoutMs: 8000 });
|
|
2330
|
+
});
|
|
2331
|
+
})();
|
|
1852
2332
|
},
|
|
1853
2333
|
runExternalCommand(name, rawInput) {
|
|
1854
2334
|
return executeRegistryCommand(name, rawInput);
|
|
@@ -2017,8 +2497,23 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2017
2497
|
if (existsSync(LEGACY_DATA_DIR)) {
|
|
2018
2498
|
lines.push(t('doctor-legacy-dir'));
|
|
2019
2499
|
}
|
|
2500
|
+
// Plugin-spec diagnostics (v0.15): the runtime generation and the
|
|
2501
|
+
// vendored registry self-check, both soft-probed (#183 discipline).
|
|
2502
|
+
const pluginHost = ctx.get('tuiPluginHost');
|
|
2503
|
+
lines.push(t('doctor-plugin-generation', { id: pluginHost?.generationId ?? t('doctor-plugin-host-missing') }));
|
|
2504
|
+
const violations = pluginHost?.selfCheck();
|
|
2505
|
+
lines.push(t('doctor-plugin-registry', {
|
|
2506
|
+
state: violations === undefined ? t('doctor-plugin-host-missing') : violations.length === 0 ? '✓' : `✗ ${violations.length}`,
|
|
2507
|
+
}));
|
|
2020
2508
|
return lines;
|
|
2021
2509
|
},
|
|
2510
|
+
pluginsInfo(args) {
|
|
2511
|
+
const host = ctx.get('tuiPluginHost');
|
|
2512
|
+
return pluginsInfoLines(args, {
|
|
2513
|
+
grants: host?.grants ?? currentGrantStore(),
|
|
2514
|
+
host: host?.describe(),
|
|
2515
|
+
});
|
|
2516
|
+
},
|
|
2022
2517
|
async listSubagents() {
|
|
2023
2518
|
const subagents = ctx.get('subagents');
|
|
2024
2519
|
if (!subagents)
|
|
@@ -2372,10 +2867,21 @@ export function createChannel(ctx, initialAgent, options) {
|
|
|
2372
2867
|
},
|
|
2373
2868
|
});
|
|
2374
2869
|
skillCommands.set(name, { dispose, description });
|
|
2870
|
+
ctx.get('tuiEffectLedger')?.record({ operation: 'create', resource: { kind: 'command', id: name }, result: 'applied' }, ctx);
|
|
2375
2871
|
}
|
|
2376
2872
|
catch (error) {
|
|
2873
|
+
// C-041: a duplicate registration arrives as a plain-message Error
|
|
2874
|
+
// from dsh-commands; map it onto the contract code before handling
|
|
2875
|
+
// (the refusal path itself is unchanged).
|
|
2876
|
+
const mapped = mapCommandError(error);
|
|
2377
2877
|
skillCommandsRefused.add(name);
|
|
2378
|
-
ctx.logger.warn(`skill commands: "${name}" not registrable: %o`,
|
|
2878
|
+
ctx.logger.warn(`skill commands: "${name}" not registrable%s: %o`, hasCommandErrorCode(mapped, 'DUPLICATE_CONTRIBUTION_ID') ? ' (DUPLICATE_CONTRIBUTION_ID)' : '', mapped);
|
|
2879
|
+
ctx.get('tuiEffectLedger')?.record({
|
|
2880
|
+
operation: 'create',
|
|
2881
|
+
resource: { kind: 'command', id: name },
|
|
2882
|
+
result: 'failed',
|
|
2883
|
+
errorCode: hasCommandErrorCode(mapped, 'DUPLICATE_CONTRIBUTION_ID') ? 'DUPLICATE_CONTRIBUTION_ID' : 'COMMAND_FAILED',
|
|
2884
|
+
}, ctx);
|
|
2379
2885
|
}
|
|
2380
2886
|
}
|
|
2381
2887
|
};
|
|
@@ -2541,16 +3047,60 @@ ${output}
|
|
|
2541
3047
|
}
|
|
2542
3048
|
return streaming;
|
|
2543
3049
|
};
|
|
2544
|
-
|
|
3050
|
+
/** Latest reasoning row keyed by its (turn, step) — lets a resumed
|
|
3051
|
+
* mid-step stream REVIVE the row the replay sealed (crash-orphan tail:
|
|
3052
|
+
* replay folds the partial row, live continuation chunks would
|
|
3053
|
+
* otherwise open a SECOND row for the same step, splitting one
|
|
3054
|
+
* thinking block in two). */
|
|
3055
|
+
let lastReasoningRow;
|
|
3056
|
+
const ensureReasoning = (seq, turn, step) => {
|
|
2545
3057
|
if (reasoning === undefined) {
|
|
3058
|
+
// Same-step revive: the sealed row is this step's thinking — continue
|
|
3059
|
+
// it (durationMs carried over via reasoningStart back-dating).
|
|
3060
|
+
if (lastReasoningRow !== undefined &&
|
|
3061
|
+
turn !== undefined &&
|
|
3062
|
+
lastReasoningRow.turn === turn &&
|
|
3063
|
+
lastReasoningRow.step === step) {
|
|
3064
|
+
reasoning = lastReasoningRow.row;
|
|
3065
|
+
reasoning.streaming = true;
|
|
3066
|
+
const sealedIdx = sealedReasoning.indexOf(reasoning);
|
|
3067
|
+
if (sealedIdx !== -1)
|
|
3068
|
+
sealedReasoning.splice(sealedIdx, 1);
|
|
3069
|
+
reasoningStart = Date.now() - (reasoning.durationMs ?? 0);
|
|
3070
|
+
logForDebugging('thinking: revived sealed reasoning row for same step');
|
|
3071
|
+
return reasoning;
|
|
3072
|
+
}
|
|
2546
3073
|
reasoningStart = Date.now();
|
|
2547
3074
|
reasoning = { id: nextRowId, kind: 'reasoning', text: '', streaming: true, ...seq !== undefined ? { seq } : {} };
|
|
2548
3075
|
nextRowId += 1;
|
|
2549
3076
|
state.rows.push(reasoning);
|
|
2550
3077
|
logForDebugging('thinking: reasoning row open (expanded)');
|
|
2551
3078
|
}
|
|
3079
|
+
if (turn !== undefined && step !== undefined) {
|
|
3080
|
+
lastReasoningRow = { row: reasoning, turn, step };
|
|
3081
|
+
}
|
|
2552
3082
|
return reasoning;
|
|
2553
3083
|
};
|
|
3084
|
+
/** Fold the live reasoning preview the moment the model moves PAST
|
|
3085
|
+
* thinking — the answer's first text token or a tool call — not at
|
|
3086
|
+
* `assistant/message` (end of step). A long reply pushes the thinking
|
|
3087
|
+
* block into terminal scrollback long before the message seals, and
|
|
3088
|
+
* scrollback rows cannot be repainted (the cursor cannot reach them),
|
|
3089
|
+
* so a late fold leaves a stale unfolded preview frozen above the
|
|
3090
|
+
* window — the user scrolls up and the thinking looks "not folded".
|
|
3091
|
+
* Folding while the block still sits in the live window keeps the
|
|
3092
|
+
* shrink inside the diff engine's reachable region. Preview mode only
|
|
3093
|
+
* (`full` holds every block open until turn settle by design). */
|
|
3094
|
+
const foldLiveReasoning = (where) => {
|
|
3095
|
+
if (reasoning === undefined || state.thinkingFold !== 'preview')
|
|
3096
|
+
return;
|
|
3097
|
+
const duration = Math.max(0, Date.now() - reasoningStart);
|
|
3098
|
+
reasoning.durationMs = duration;
|
|
3099
|
+
reasoning.streaming = false;
|
|
3100
|
+
sealedReasoning.push(reasoning);
|
|
3101
|
+
reasoning = undefined;
|
|
3102
|
+
logForDebugging(`thinking: folded at ${where} (${duration}ms)`);
|
|
3103
|
+
};
|
|
2554
3104
|
const settleStreaming = () => {
|
|
2555
3105
|
if (streaming !== undefined)
|
|
2556
3106
|
streaming.streaming = false;
|
|
@@ -2616,6 +3166,24 @@ ${output}
|
|
|
2616
3166
|
};
|
|
2617
3167
|
}
|
|
2618
3168
|
};
|
|
3169
|
+
/** True while the durable transcript is being replayed (boot /resume /
|
|
3170
|
+
* rewind / model-switch fork). The assistant/message reasoning-rebuild
|
|
3171
|
+
* branch below must run ONLY on this path: in a live stream the chunks
|
|
3172
|
+
* already created the reasoning row, and foldLiveReasoning clears the
|
|
3173
|
+
* `reasoning` handle before assistant/message arrives — so
|
|
3174
|
+
* `reasoning === undefined` alone cannot tell replay from live, and
|
|
3175
|
+
* using it would rebuild a second thinking block per step. */
|
|
3176
|
+
let replaying = false;
|
|
3177
|
+
const replayEvents = (events) => {
|
|
3178
|
+
replaying = true;
|
|
3179
|
+
try {
|
|
3180
|
+
for (const event of prepareReplayEvents(events))
|
|
3181
|
+
renderEvent(event);
|
|
3182
|
+
}
|
|
3183
|
+
finally {
|
|
3184
|
+
replaying = false;
|
|
3185
|
+
}
|
|
3186
|
+
};
|
|
2619
3187
|
const renderEvent = (event) => {
|
|
2620
3188
|
switch (event.type) {
|
|
2621
3189
|
case 'user/message': {
|
|
@@ -2697,13 +3265,18 @@ ${output}
|
|
|
2697
3265
|
const chunk = event.data.chunk;
|
|
2698
3266
|
if (chunk.type === 'text-delta') {
|
|
2699
3267
|
if (chunk.text) {
|
|
3268
|
+
// Fold the thinking preview while it is still in the live
|
|
3269
|
+
// window (see foldLiveReasoning) — before this text grows the
|
|
3270
|
+
// transcript and pushes the block into scrollback.
|
|
3271
|
+
foldLiveReasoning('first text token');
|
|
2700
3272
|
ensureStreaming(event.seq).text += chunk.text;
|
|
2701
3273
|
state.responseChars += chunk.text.length;
|
|
2702
3274
|
}
|
|
2703
3275
|
}
|
|
2704
3276
|
else if (chunk.type === 'reasoning-delta') {
|
|
2705
|
-
if (chunk.text)
|
|
2706
|
-
ensureReasoning(event.seq).text += chunk.text;
|
|
3277
|
+
if (chunk.text) {
|
|
3278
|
+
ensureReasoning(event.seq, event.data.turn, event.data.step).text += chunk.text;
|
|
3279
|
+
}
|
|
2707
3280
|
}
|
|
2708
3281
|
const step = tpsStep;
|
|
2709
3282
|
if (step !== undefined &&
|
|
@@ -2724,6 +3297,30 @@ ${output}
|
|
|
2724
3297
|
}
|
|
2725
3298
|
case 'assistant/message': {
|
|
2726
3299
|
const text = textOf(event.data.message.content);
|
|
3300
|
+
// Replay without chunk deltas (prepareReplayEvents drops settled
|
|
3301
|
+
// ones): rebuild the reasoning row from the sealed message's
|
|
3302
|
+
// reasoning blocks. Replay-only — gated on the `replaying` flag,
|
|
3303
|
+
// not on `reasoning === undefined`: a live stream's chunks already
|
|
3304
|
+
// created the row, and foldLiveReasoning has cleared the `reasoning`
|
|
3305
|
+
// handle by the time this event lands, so the undefined check alone
|
|
3306
|
+
// would rebuild a duplicate thinking block per step. Pushed BEFORE
|
|
3307
|
+
// the assistant row so the transcript order matches the live
|
|
3308
|
+
// stream; settled (folded) immediately, durationMs unknown without
|
|
3309
|
+
// a live clock.
|
|
3310
|
+
if (replaying && reasoning === undefined) {
|
|
3311
|
+
const reasoningText = event.data.message.content
|
|
3312
|
+
.map(block => (block.type === 'reasoning' ? block.text : ''))
|
|
3313
|
+
.join('');
|
|
3314
|
+
if (reasoningText !== '') {
|
|
3315
|
+
state.rows.push({
|
|
3316
|
+
id: nextRowId,
|
|
3317
|
+
kind: 'reasoning',
|
|
3318
|
+
text: reasoningText,
|
|
3319
|
+
seq: event.seq,
|
|
3320
|
+
});
|
|
3321
|
+
nextRowId += 1;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
2727
3324
|
// Reasoning/tool-only steps emit no text: creating an assistant row
|
|
2728
3325
|
// anyway leaves an empty `●` bullet in the transcript. A pre-existing
|
|
2729
3326
|
// streaming row always has text (ensureStreaming is only reached on
|
|
@@ -2737,10 +3334,16 @@ ${output}
|
|
|
2737
3334
|
}
|
|
2738
3335
|
streaming = undefined;
|
|
2739
3336
|
if (reasoning !== undefined) {
|
|
2740
|
-
//
|
|
2741
|
-
//
|
|
2742
|
-
//
|
|
3337
|
+
// Backstop fold: reasoning whose step ended with no text token
|
|
3338
|
+
// and no tool call (foldLiveReasoning handles those earlier —
|
|
3339
|
+
// while the block is still in the repaintable live window;
|
|
3340
|
+
// here a long reply may already have pushed it into scrollback,
|
|
3341
|
+
// where the shrink cannot be repainted). `full` mode
|
|
3342
|
+
// (/settings opt-in) keeps the block expanded until turn settle
|
|
3343
|
+
// — settleStreaming folds the sealed rows then.
|
|
2743
3344
|
reasoning.durationMs = Math.max(0, Date.now() - reasoningStart);
|
|
3345
|
+
if (state.thinkingFold === 'preview')
|
|
3346
|
+
reasoning.streaming = false;
|
|
2744
3347
|
sealedReasoning.push(reasoning);
|
|
2745
3348
|
logForDebugging(`thinking: step sealed (${reasoning.durationMs}ms), expanded until turn/end`);
|
|
2746
3349
|
}
|
|
@@ -2809,6 +3412,10 @@ ${output}
|
|
|
2809
3412
|
// no card is a no-op below.
|
|
2810
3413
|
if (event.data.name === 'ask_user_question')
|
|
2811
3414
|
break;
|
|
3415
|
+
// Reasoning that led to a tool call is done thinking — fold the
|
|
3416
|
+
// preview now, before the tool card grows the transcript past it
|
|
3417
|
+
// (see foldLiveReasoning).
|
|
3418
|
+
foldLiveReasoning('tool call');
|
|
2812
3419
|
const card = {
|
|
2813
3420
|
id: nextRowId,
|
|
2814
3421
|
kind: 'tool',
|
|
@@ -2976,13 +3583,36 @@ ${output}
|
|
|
2976
3583
|
text: t('agent-preset-switched', { preset: data.agentPreset ?? 'unknown' }),
|
|
2977
3584
|
});
|
|
2978
3585
|
nextRowId += 1;
|
|
3586
|
+
break;
|
|
3587
|
+
}
|
|
3588
|
+
// Custom plugin events (tuiRenderers seam): a registered renderer
|
|
3589
|
+
// maps the payload to text rows — title as a local row, body as
|
|
3590
|
+
// preview-clipped local-output rows, same shape pushLocal uses.
|
|
3591
|
+
// Runs on the live stream AND on replay (resume/rewind), so the
|
|
3592
|
+
// projection must stay total; the runtime isolates renderer
|
|
3593
|
+
// crashes per type.
|
|
3594
|
+
if (rendererRuntime !== undefined) {
|
|
3595
|
+
const rendered = rendererRuntime.render(event.type, event.data);
|
|
3596
|
+
if (rendered !== undefined) {
|
|
3597
|
+
if (rendered.title !== undefined && rendered.title !== '') {
|
|
3598
|
+
state.rows.push({ id: nextRowId, kind: 'local', text: rendered.title });
|
|
3599
|
+
nextRowId += 1;
|
|
3600
|
+
}
|
|
3601
|
+
for (const line of rendered.lines) {
|
|
3602
|
+
state.rows.push({
|
|
3603
|
+
id: nextRowId,
|
|
3604
|
+
kind: 'local-output',
|
|
3605
|
+
text: preview(String(line), LOCAL_OUTPUT_LIMIT),
|
|
3606
|
+
});
|
|
3607
|
+
nextRowId += 1;
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
2979
3610
|
}
|
|
2980
3611
|
break;
|
|
2981
3612
|
}
|
|
2982
3613
|
};
|
|
2983
3614
|
// Replay the durable transcript first, then follow live events.
|
|
2984
|
-
|
|
2985
|
-
renderEvent(event);
|
|
3615
|
+
replayEvents(agent.session.events);
|
|
2986
3616
|
settleStreaming();
|
|
2987
3617
|
// Attached to an idle agent: any replayed turn/start belongs to a previous
|
|
2988
3618
|
// session run, so the spinner must not come up on boot.
|
|
@@ -3113,6 +3743,10 @@ ${output}
|
|
|
3113
3743
|
ctx.on('session/event', (session, event) => {
|
|
3114
3744
|
if (session !== agent.session)
|
|
3115
3745
|
return;
|
|
3746
|
+
// Observation broker (C-042): maps user/message + assistant/message
|
|
3747
|
+
// into grant-gated envelopes; every other event type is a no-op, and
|
|
3748
|
+
// publish never throws into this arm.
|
|
3749
|
+
messageObserver?.publish(session, event);
|
|
3116
3750
|
activityTracker.onSessionEvent(event);
|
|
3117
3751
|
renderWorkingActivity();
|
|
3118
3752
|
// Mode-affecting atoms fold into the Shift+Tab mode indicator the
|
|
@@ -3274,47 +3908,60 @@ function usageOutputTokens(usage) {
|
|
|
3274
3908
|
: undefined;
|
|
3275
3909
|
}
|
|
3276
3910
|
/**
|
|
3277
|
-
*
|
|
3278
|
-
*
|
|
3279
|
-
*
|
|
3280
|
-
*
|
|
3281
|
-
*
|
|
3911
|
+
* `@` file listing through the leaf's fs service (dsh-fs-local): skips
|
|
3912
|
+
* VCS/dependency/build dirs and rotates across directory listings so one
|
|
3913
|
+
* large subtree cannot consume the global MAX_FILES budget. That budget also
|
|
3914
|
+
* bounds directory reads without imposing an arbitrary source-tree depth.
|
|
3915
|
+
* Relative directories retain the trailing `/` that FileSuggestions expects.
|
|
3916
|
+
* Unreadable subtrees are skipped, not fatal.
|
|
3282
3917
|
*/
|
|
3283
3918
|
async function listFilesDeep(fs, root) {
|
|
3284
3919
|
if (!fs)
|
|
3285
3920
|
return [];
|
|
3286
3921
|
const out = [];
|
|
3287
|
-
const SKIP = new Set(['node_modules', '.git', '.hg', '.svn', '.DS_Store', 'dist'
|
|
3288
|
-
const
|
|
3922
|
+
const SKIP = new Set(['node_modules', '.git', '.hg', '.svn', '.DS_Store', 'dist']);
|
|
3923
|
+
const BUILD_DIR = /^(?:build(?:[-_].*)?|cmake-build(?:[-_].*)?)$/i;
|
|
3289
3924
|
const MAX_FILES = 100;
|
|
3290
|
-
const
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3925
|
+
const directories = [{ dir: root, prefix: '', index: 0 }];
|
|
3926
|
+
while (directories.length > 0 && out.length < MAX_FILES) {
|
|
3927
|
+
const current = directories.shift();
|
|
3928
|
+
if (!current)
|
|
3929
|
+
continue;
|
|
3930
|
+
if (!current.entries) {
|
|
3931
|
+
try {
|
|
3932
|
+
const target = await fs.resolve(current.dir);
|
|
3933
|
+
current.entries = await fs.listDir(target);
|
|
3934
|
+
}
|
|
3935
|
+
catch {
|
|
3936
|
+
continue; // unreadable subtree — skip
|
|
3937
|
+
}
|
|
3300
3938
|
}
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
if (SKIP.has(
|
|
3939
|
+
let entry;
|
|
3940
|
+
while (current.index < current.entries.length) {
|
|
3941
|
+
const candidate = current.entries[current.index++];
|
|
3942
|
+
if (SKIP.has(candidate.name) || BUILD_DIR.test(candidate.name))
|
|
3305
3943
|
continue;
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3944
|
+
entry = candidate;
|
|
3945
|
+
break;
|
|
3946
|
+
}
|
|
3947
|
+
if (!entry)
|
|
3948
|
+
continue;
|
|
3949
|
+
if (current.index < current.entries.length)
|
|
3950
|
+
directories.push(current);
|
|
3951
|
+
const rel = current.prefix ? `${current.prefix}/${entry.name}` : entry.name;
|
|
3952
|
+
if (entry.type === 'directory') {
|
|
3953
|
+
out.push(`${rel}/`);
|
|
3954
|
+
directories.push({
|
|
3309
3955
|
// oxlint-disable-next-line typescript/no-unnecessary-condition -- runtime guard: symlink targets optional
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
}
|
|
3956
|
+
dir: entry.target?.displayPath ?? join(current.dir, entry.name),
|
|
3957
|
+
prefix: rel,
|
|
3958
|
+
index: 0,
|
|
3959
|
+
});
|
|
3315
3960
|
}
|
|
3316
|
-
|
|
3317
|
-
|
|
3961
|
+
else if (entry.type === 'file') {
|
|
3962
|
+
out.push(rel);
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3318
3965
|
return out;
|
|
3319
3966
|
}
|
|
3320
3967
|
/** One attached file's contribution is capped so an absent-minded `@` of a
|