@deepseek-harness-tui/dsh-tui 0.10.2 → 0.11.1
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 +175 -123
- package/bin/dsh-tui.js +748 -22
- package/cordis.patch.yml +42 -13
- package/cordis.yml +5 -2
- package/lib/types/activityPrefs.d.ts +24 -0
- package/lib/types/activityPrefs.d.ts.map +1 -1
- package/lib/types/activityPrefs.js +50 -1
- package/lib/types/adapter/channel/session-projection.d.ts.map +1 -1
- package/lib/types/adapter/channel/session-projection.js +2 -0
- package/lib/types/adapter/channel/state.d.ts.map +1 -1
- package/lib/types/adapter/channel/state.js +3 -1
- package/lib/types/adapter/channel/ui-policy.d.ts +4 -1
- package/lib/types/adapter/channel/ui-policy.d.ts.map +1 -1
- package/lib/types/adapter/channel/ui-policy.js +6 -2
- package/lib/types/adapter/kernel/host-facade.d.ts.map +1 -1
- package/lib/types/adapter/kernel/host-facade.js +1 -0
- package/lib/types/adapter/kernel/replay.d.ts.map +1 -1
- package/lib/types/adapter/kernel/replay.js +3 -0
- package/lib/types/adapter/kernel/runtime.d.ts.map +1 -1
- package/lib/types/adapter/kernel/runtime.js +2 -0
- package/lib/types/adapter/kernel/slices/workspace.d.ts.map +1 -1
- package/lib/types/adapter/kernel/slices/workspace.js +1 -0
- package/lib/types/adapter/ports/channel-ui.d.ts +29 -5
- package/lib/types/adapter/ports/channel-ui.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-view.d.ts +66 -12
- package/lib/types/adapter/ports/channel-view.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel-workspace.d.ts +23 -0
- package/lib/types/adapter/ports/channel-workspace.d.ts.map +1 -1
- package/lib/types/adapter/ports/channel.d.ts +4 -1
- package/lib/types/adapter/ports/channel.d.ts.map +1 -1
- package/lib/types/adapter/ports/workspace.d.ts +2 -0
- package/lib/types/adapter/ports/workspace.d.ts.map +1 -1
- package/lib/types/adapter/upstream/workspace-driver.d.ts.map +1 -1
- package/lib/types/adapter/upstream/workspace-driver.js +10 -2
- package/lib/types/commands.d.ts.map +1 -1
- package/lib/types/commands.js +1 -0
- package/lib/types/components/ActivityLine.d.ts +7 -2
- package/lib/types/components/ActivityLine.d.ts.map +1 -1
- package/lib/types/components/ActivityLine.js +0 -8
- package/lib/types/components/ContextBarView.d.ts +15 -7
- package/lib/types/components/ContextBarView.d.ts.map +1 -1
- package/lib/types/components/ContextBarView.js +26 -22
- package/lib/types/components/ImagePreviewOverlay.d.ts.map +1 -1
- package/lib/types/components/ImagePreviewOverlay.js +11 -5
- package/lib/types/components/Markdown.d.ts +11 -1
- package/lib/types/components/Markdown.d.ts.map +1 -1
- package/lib/types/components/Markdown.js +20 -2
- package/lib/types/components/MermaidDiagram.d.ts +13 -0
- package/lib/types/components/MermaidDiagram.d.ts.map +1 -0
- package/lib/types/components/MermaidDiagram.js +61 -0
- package/lib/types/components/MessageList.d.ts +26 -1
- package/lib/types/components/MessageList.d.ts.map +1 -1
- package/lib/types/components/MessageList.js +74 -8
- package/lib/types/components/PageMargin.d.ts +1 -1
- package/lib/types/components/PromptInput.d.ts +37 -1
- package/lib/types/components/PromptInput.d.ts.map +1 -1
- package/lib/types/components/PromptInput.js +291 -30
- package/lib/types/components/ScrollbarGutter.d.ts +20 -5
- package/lib/types/components/ScrollbarGutter.d.ts.map +1 -1
- package/lib/types/components/ScrollbarGutter.js +88 -10
- package/lib/types/components/SplitDiffView.d.ts.map +1 -1
- package/lib/types/components/SplitDiffView.js +2 -1
- package/lib/types/components/StreamingMarkdown.d.ts.map +1 -1
- package/lib/types/components/StreamingMarkdown.js +30 -28
- package/lib/types/components/SubagentCard.d.ts.map +1 -1
- package/lib/types/components/SubagentCard.js +12 -6
- package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
- package/lib/types/components/SubagentDetailScene.js +8 -3
- package/lib/types/components/Tooltip.d.ts.map +1 -1
- package/lib/types/components/Tooltip.js +45 -7
- package/lib/types/components/design-system/HintLine.d.ts +1 -2
- package/lib/types/components/design-system/HintLine.d.ts.map +1 -1
- package/lib/types/components/design-system/HintLine.js +1 -2
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts +2 -1
- package/lib/types/components/messages/AssistantThinkingMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantThinkingMessage.js +2 -1
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts +2 -2
- package/lib/types/components/messages/AssistantToolUseMessage.d.ts.map +1 -1
- package/lib/types/components/messages/AssistantToolUseMessage.js +30 -26
- package/lib/types/components/messages/UserPromptMessage.d.ts.map +1 -1
- package/lib/types/components/messages/UserPromptMessage.js +5 -7
- package/lib/types/components/messages/useImageInspection.d.ts.map +1 -1
- package/lib/types/components/messages/useImageInspection.js +19 -9
- package/lib/types/components/promptDraftCache.d.ts +102 -0
- package/lib/types/components/promptDraftCache.d.ts.map +1 -0
- package/lib/types/components/promptDraftCache.js +76 -0
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts +17 -1
- package/lib/types/components/questions/AskUserQuestionPanel.d.ts.map +1 -1
- package/lib/types/components/questions/AskUserQuestionPanel.js +40 -7
- package/lib/types/components/questions/QuestionMinimizedBar.d.ts +12 -0
- package/lib/types/components/questions/QuestionMinimizedBar.d.ts.map +1 -0
- package/lib/types/components/questions/QuestionMinimizedBar.js +43 -0
- package/lib/types/components/sessions/SessionListRow.d.ts +23 -1
- package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -1
- package/lib/types/components/sessions/SessionListRow.js +52 -5
- package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts +49 -0
- package/lib/types/components/workspaces/HomeWorkspaceRow.d.ts.map +1 -0
- package/lib/types/components/workspaces/HomeWorkspaceRow.js +50 -0
- package/lib/types/dsh-adapter/activity-store.d.ts +221 -0
- package/lib/types/dsh-adapter/activity-store.d.ts.map +1 -0
- package/lib/types/dsh-adapter/activity-store.js +359 -0
- package/lib/types/dsh-adapter/bundled-presets.d.ts +10 -0
- package/lib/types/dsh-adapter/bundled-presets.d.ts.map +1 -0
- package/lib/types/dsh-adapter/bundled-presets.js +80 -0
- package/lib/types/dsh-adapter/channel/action-readiness.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/action-readiness.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/action-readiness.js +3 -0
- package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/agent-view-projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/agent-view-projection.js +19 -2
- package/lib/types/dsh-adapter/channel/background-action.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/background-action.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/background-action.js +53 -29
- package/lib/types/dsh-adapter/channel/binding-events.d.ts +8 -6
- package/lib/types/dsh-adapter/channel/binding-events.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/binding-events.js +5 -4
- package/lib/types/dsh-adapter/channel/binding.d.ts +19 -2
- package/lib/types/dsh-adapter/channel/binding.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/binding.js +78 -18
- package/lib/types/dsh-adapter/channel/compaction.d.ts +0 -1
- package/lib/types/dsh-adapter/channel/compaction.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/compaction.js +0 -2
- package/lib/types/dsh-adapter/channel/composer-images.d.ts +5 -0
- package/lib/types/dsh-adapter/channel/composer-images.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/composer-images.js +105 -6
- package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts +8 -0
- package/lib/types/dsh-adapter/channel/context-bookkeeping.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/context-bookkeeping.js +3 -2
- package/lib/types/dsh-adapter/channel/ide-selection.d.ts +58 -0
- package/lib/types/dsh-adapter/channel/ide-selection.d.ts.map +1 -0
- package/lib/types/dsh-adapter/channel/ide-selection.js +226 -0
- package/lib/types/dsh-adapter/channel/input-delivery.d.ts +6 -1
- package/lib/types/dsh-adapter/channel/input-delivery.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/input-delivery.js +9 -1
- package/lib/types/dsh-adapter/channel/local-actions.d.ts +3 -19
- package/lib/types/dsh-adapter/channel/local-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/local-actions.js +2 -1
- package/lib/types/dsh-adapter/channel/model-actions.d.ts +4 -1
- package/lib/types/dsh-adapter/channel/model-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/model-actions.js +82 -6
- package/lib/types/dsh-adapter/channel/model-switch.d.ts +1 -2
- package/lib/types/dsh-adapter/channel/model-switch.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/model-switch.js +49 -31
- package/lib/types/dsh-adapter/channel/projection.d.ts +4 -0
- package/lib/types/dsh-adapter/channel/projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/projection.js +29 -19
- package/lib/types/dsh-adapter/channel/reports.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/reports.js +4 -12
- package/lib/types/dsh-adapter/channel/session-adoption.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/session-adoption.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-adoption.js +1 -0
- package/lib/types/dsh-adapter/channel/session-fork.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-fork.js +26 -2
- package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts +4 -1
- package/lib/types/dsh-adapter/channel/session-live-adoption.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-live-adoption.js +2 -0
- package/lib/types/dsh-adapter/channel/session-metadata.d.ts +10 -0
- package/lib/types/dsh-adapter/channel/session-metadata.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-metadata.js +6 -4
- package/lib/types/dsh-adapter/channel/session-reset.d.ts +1 -1
- package/lib/types/dsh-adapter/channel/session-reset.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-reset.js +0 -1
- package/lib/types/dsh-adapter/channel/session-resume.d.ts +12 -3
- package/lib/types/dsh-adapter/channel/session-resume.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-resume.js +292 -94
- package/lib/types/dsh-adapter/channel/session-rewind.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-rewind.js +34 -16
- package/lib/types/dsh-adapter/channel/session-tree-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/session-tree-actions.js +19 -3
- package/lib/types/dsh-adapter/channel/settings-host.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/settings-host.js +4 -3
- package/lib/types/dsh-adapter/channel/state.d.ts +6 -1
- package/lib/types/dsh-adapter/channel/state.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/state.js +3 -3
- package/lib/types/dsh-adapter/channel/subagent-projection.d.ts +21 -1
- package/lib/types/dsh-adapter/channel/subagent-projection.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/subagent-projection.js +173 -6
- package/lib/types/dsh-adapter/channel/transcript.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/transcript.js +6 -12
- package/lib/types/dsh-adapter/channel/types.d.ts +6 -1
- package/lib/types/dsh-adapter/channel/types.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.d.ts +4 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel/workspace-actions.js +41 -1
- package/lib/types/dsh-adapter/channel.d.ts +1 -1
- package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
- package/lib/types/dsh-adapter/channel.js +82 -20
- package/lib/types/dsh-adapter/compat/index.d.ts +1 -1
- package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/index.js +1 -1
- package/lib/types/dsh-adapter/compat/messages.d.ts +15 -0
- package/lib/types/dsh-adapter/compat/messages.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/messages.js +15 -0
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts +18 -0
- package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
- package/lib/types/dsh-adapter/compat/sessionLog.js +18 -3
- package/lib/types/dsh-adapter/compat/settings.d.ts +35 -0
- package/lib/types/dsh-adapter/compat/settings.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/settings.js +57 -0
- package/lib/types/dsh-adapter/compat/shell.d.ts +25 -0
- package/lib/types/dsh-adapter/compat/shell.d.ts.map +1 -0
- package/lib/types/dsh-adapter/compat/shell.js +11 -0
- package/lib/types/dsh-adapter/contract.d.ts +5 -4
- package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
- package/lib/types/dsh-adapter/contract.js +17 -6
- package/lib/types/dsh-adapter/ide-channel.d.ts +224 -0
- package/lib/types/dsh-adapter/ide-channel.d.ts.map +1 -0
- package/lib/types/dsh-adapter/ide-channel.js +556 -0
- package/lib/types/dsh-adapter/index.d.ts +18 -3
- package/lib/types/dsh-adapter/index.d.ts.map +1 -1
- package/lib/types/dsh-adapter/index.js +32 -5
- package/lib/types/dsh-adapter/plugin.d.ts +4 -1
- package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
- package/lib/types/dsh-adapter/plugin.js +197 -49
- package/lib/types/dsh-adapter/preset-resolution.d.ts +8 -0
- package/lib/types/dsh-adapter/preset-resolution.d.ts.map +1 -1
- package/lib/types/dsh-adapter/preset-resolution.js +10 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts +53 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.d.ts.map +1 -0
- package/lib/types/dsh-adapter/session-mount-heartbeat.js +100 -0
- package/lib/types/dsh-adapter/sessionTree.d.ts.map +1 -1
- package/lib/types/dsh-adapter/sessionTree.js +3 -2
- package/lib/types/dsh-adapter/settings-sections.d.ts.map +1 -1
- package/lib/types/dsh-adapter/settings-sections.js +7 -4
- package/lib/types/dsh-adapter/subagents.d.ts +16 -3
- package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
- package/lib/types/dsh-adapter/subagents.js +92 -29
- package/lib/types/dsh-adapter/workspaces.d.ts +46 -3
- package/lib/types/dsh-adapter/workspaces.d.ts.map +1 -1
- package/lib/types/dsh-adapter/workspaces.js +116 -0
- package/lib/types/effortPrefs.d.ts +15 -3
- package/lib/types/effortPrefs.d.ts.map +1 -1
- package/lib/types/effortPrefs.js +37 -3
- package/lib/types/history.d.ts +13 -1
- package/lib/types/history.d.ts.map +1 -1
- package/lib/types/history.js +15 -2
- package/lib/types/homePrefs.d.ts +15 -0
- package/lib/types/homePrefs.d.ts.map +1 -0
- package/lib/types/homePrefs.js +53 -0
- package/lib/types/i18n.d.ts +347 -297
- package/lib/types/i18n.d.ts.map +1 -1
- package/lib/types/i18n.js +133 -85
- package/lib/types/ink/colorize.d.ts +10 -0
- package/lib/types/ink/colorize.d.ts.map +1 -1
- package/lib/types/ink/colorize.js +21 -0
- package/lib/types/ink/components/ScrollBox.d.ts.map +1 -1
- package/lib/types/ink/components/ScrollBox.js +12 -1
- package/lib/types/ink/dom.d.ts +1 -0
- package/lib/types/ink/dom.d.ts.map +1 -1
- package/lib/types/ink/hooks/use-copy-on-select.d.ts +5 -1
- package/lib/types/ink/hooks/use-copy-on-select.d.ts.map +1 -1
- package/lib/types/ink/hooks/use-copy-on-select.js +15 -1
- package/lib/types/ink/ink.d.ts.map +1 -1
- package/lib/types/ink/ink.js +36 -1
- package/lib/types/ink/log-update.d.ts.map +1 -1
- package/lib/types/ink/log-update.js +19 -1
- package/lib/types/ink/output.d.ts +10 -1
- package/lib/types/ink/output.d.ts.map +1 -1
- package/lib/types/ink/output.js +109 -17
- package/lib/types/ink/render-border.d.ts +6 -1
- package/lib/types/ink/render-border.d.ts.map +1 -1
- package/lib/types/ink/render-border.js +25 -9
- package/lib/types/ink/render-node-to-output.d.ts.map +1 -1
- package/lib/types/ink/render-node-to-output.js +48 -6
- package/lib/types/ink/renderer.d.ts +6 -0
- package/lib/types/ink/renderer.d.ts.map +1 -1
- package/lib/types/ink/renderer.js +2 -1
- package/lib/types/ink/selection.d.ts +88 -0
- package/lib/types/ink/selection.d.ts.map +1 -1
- package/lib/types/ink/selection.js +164 -0
- package/lib/types/ink/sixel-graphics.d.ts +19 -1
- package/lib/types/ink/sixel-graphics.d.ts.map +1 -1
- package/lib/types/ink/sixel-graphics.js +124 -12
- package/lib/types/ink/terminal-image.d.ts +18 -1
- package/lib/types/ink/terminal-image.d.ts.map +1 -1
- package/lib/types/ink/terminal.d.ts +17 -0
- package/lib/types/ink/terminal.d.ts.map +1 -1
- package/lib/types/ink/terminal.js +20 -1
- package/lib/types/screens/Chat.d.ts +20 -1
- package/lib/types/screens/Chat.d.ts.map +1 -1
- package/lib/types/screens/Chat.js +296 -76
- package/lib/types/screens/SessionSupervisor.d.ts +84 -0
- package/lib/types/screens/SessionSupervisor.d.ts.map +1 -0
- package/lib/types/screens/SessionSupervisor.js +350 -0
- package/lib/types/screens/SessionTree.js +2 -2
- package/lib/types/screens/Settings.d.ts.map +1 -1
- package/lib/types/screens/Settings.js +32 -9
- package/lib/types/screens/StatusLine.d.ts +18 -1
- package/lib/types/screens/StatusLine.d.ts.map +1 -1
- package/lib/types/screens/StatusLine.js +38 -19
- package/lib/types/screens/StatusMetrics.d.ts +79 -22
- package/lib/types/screens/StatusMetrics.d.ts.map +1 -1
- package/lib/types/screens/StatusMetrics.js +153 -88
- package/lib/types/screens/sessionSupervisor/model.d.ts +83 -0
- package/lib/types/screens/sessionSupervisor/model.d.ts.map +1 -0
- package/lib/types/screens/sessionSupervisor/model.js +69 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts +120 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.d.ts.map +1 -0
- package/lib/types/screens/sessionSupervisor/useSessionSupervisor.js +593 -0
- package/lib/types/sessionMounts.d.ts +272 -0
- package/lib/types/sessionMounts.d.ts.map +1 -0
- package/lib/types/sessionMounts.js +706 -0
- package/lib/types/sessions/format.d.ts.map +1 -1
- package/lib/types/sessions/format.js +4 -1
- package/lib/types/sessions/resumeFailure.d.ts +38 -0
- package/lib/types/sessions/resumeFailure.d.ts.map +1 -0
- package/lib/types/sessions/resumeFailure.js +58 -0
- package/lib/types/terminal-utils/mermaid.d.ts +44 -0
- package/lib/types/terminal-utils/mermaid.d.ts.map +1 -0
- package/lib/types/terminal-utils/mermaid.js +83 -0
- package/lib/types/tips.d.ts.map +1 -1
- package/lib/types/tips.js +175 -19
- package/lib/types/tuiDisplayPrefs.d.ts +7 -9
- package/lib/types/tuiDisplayPrefs.d.ts.map +1 -1
- package/lib/types/tuiDisplayPrefs.js +31 -28
- package/lib/types/update.d.ts +61 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +194 -0
- package/lib/types/utils/fold-long-lines.d.ts +1 -1
- package/lib/types/utils/fold-long-lines.js +1 -1
- package/lib/types/utils/imageResize.d.ts +90 -0
- package/lib/types/utils/imageResize.d.ts.map +1 -0
- package/lib/types/utils/imageResize.js +272 -0
- package/lib/types/utils/keymap.d.ts +5 -1
- package/lib/types/utils/keymap.d.ts.map +1 -1
- package/lib/types/utils/keymap.js +23 -0
- package/lib/types/working-activity.d.ts +10 -9
- package/lib/types/working-activity.d.ts.map +1 -1
- package/lib/types/working-activity.js +17 -10
- 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/@deepseek-harness-tui/dsh-auth/package.json +10 -10
- package/package.json +126 -105
- package/presets/liangshen/.dsh-tui-managed.json +1 -1
- package/presets/liangshen/agent.cordis.yml +15 -0
- package/lib/types/components/design-system/KeyboardShortcutHint.d.ts +0 -19
- package/lib/types/components/design-system/KeyboardShortcutHint.d.ts.map +0 -1
- package/lib/types/components/design-system/KeyboardShortcutHint.js +0 -14
- package/lib/types/dsh-adapter/channel/activity.d.ts +0 -20
- package/lib/types/dsh-adapter/channel/activity.d.ts.map +0 -1
- package/lib/types/dsh-adapter/channel/activity.js +0 -113
- package/lib/types/screens/AgentView.d.ts +0 -23
- package/lib/types/screens/AgentView.d.ts.map +0 -1
- package/lib/types/screens/AgentView.js +0 -546
- package/lib/types/screens/SessionBrowser.d.ts +0 -42
- package/lib/types/screens/SessionBrowser.d.ts.map +0 -1
- package/lib/types/screens/SessionBrowser.js +0 -911
- package/lib/types/terminal-utils/terminal.d.ts +0 -18
- package/lib/types/terminal-utils/terminal.d.ts.map +0 -1
- package/lib/types/terminal-utils/terminal.js +0 -96
|
@@ -1,911 +0,0 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { Box, Text, useInput, useTerminalSize } from '../ui.js';
|
|
4
|
-
import { Divider } from '../components/design-system/Divider.js';
|
|
5
|
-
import { HintLine } from '../components/design-system/HintLine.js';
|
|
6
|
-
import { SearchBox } from '../components/SearchBox.js';
|
|
7
|
-
import { PageInsetContext } from '../components/PageMargin.js';
|
|
8
|
-
import { SessionListRow } from '../components/sessions/SessionListRow.js';
|
|
9
|
-
import { SessionPreview } from '../components/sessions/SessionPreview.js';
|
|
10
|
-
import { WorkspaceListRow } from '../components/sessions/WorkspaceListRow.js';
|
|
11
|
-
import { useTerminalFocus } from '../ink/hooks/use-terminal-focus.js';
|
|
12
|
-
import { isMod, isPlainReturn, modLabel } from '../utils/modifiers.js';
|
|
13
|
-
import { formatProject, projectName, spreadRow, tailWidth, truncateWidth } from '../sessions/format.js';
|
|
14
|
-
import { stringWidth } from '../ink/stringWidth.js';
|
|
15
|
-
import { TICK, MULTIPLICATION_X } from '../terminal-utils/figures.js';
|
|
16
|
-
import { anchorTop, buildView, buildWorkspaceGroups, DEFAULT_FILTERS, moveSelection, normalizeWorkspaceCwd, seekSelectable, sessionAt, windowEnd, } from '../sessions/view.js';
|
|
17
|
-
import { t } from '../i18n.js';
|
|
18
|
-
import { readSessionPins, setSessionPinned } from '../sessionPins.js';
|
|
19
|
-
/**
|
|
20
|
-
* Rows the layout cannot do without: the header, the four-row scope/search
|
|
21
|
-
* card, the notice slot, and the hints. Everything else yields before these.
|
|
22
|
-
*/
|
|
23
|
-
const REGULAR_MANDATORY_LINES = 7;
|
|
24
|
-
const COMPACT_MANDATORY_LINES = 5;
|
|
25
|
-
/**
|
|
26
|
-
* The one remaining horizontal rule, above the hints.
|
|
27
|
-
*
|
|
28
|
-
* The search card's own border separates it from the header and the list, so
|
|
29
|
-
* the two rules that used to flank it are gone; what remains lifts the hints
|
|
30
|
-
* off the content — decoration, and the first thing a short terminal drops.
|
|
31
|
-
*/
|
|
32
|
-
const RULE_PRIORITY = [2];
|
|
33
|
-
/** Wide screens keep the working-directory rail visible beside sessions. */
|
|
34
|
-
const WORKSPACE_RAIL_MIN_COLUMNS = 120;
|
|
35
|
-
/** Content width below which preview replaces the session list. */
|
|
36
|
-
const SPLIT_MIN_COLUMNS = 100;
|
|
37
|
-
const WORKSPACE_ROW_HEIGHT = 2;
|
|
38
|
-
/**
|
|
39
|
-
* The right-click session menu, top to bottom. Index doubles as the
|
|
40
|
-
* keyboard cursor: ↑/↓ move it, Enter activates the highlighted action.
|
|
41
|
-
*/
|
|
42
|
-
const MENU_ACTIONS = ['open', 'pin', 'rename', 'delete'];
|
|
43
|
-
/** Popup size, border included; clamped to the terminal so it never clips. */
|
|
44
|
-
const MENU_WIDTH = 22;
|
|
45
|
-
const MENU_HEIGHT = MENU_ACTIONS.length + 2;
|
|
46
|
-
const MENU_LABEL_KEYS = {
|
|
47
|
-
open: 'resume-menu-open',
|
|
48
|
-
rename: 'resume-menu-rename',
|
|
49
|
-
delete: 'resume-menu-delete',
|
|
50
|
-
};
|
|
51
|
-
/** The pin item's label depends on the target's current pin state. */
|
|
52
|
-
const menuLabelKey = (action, pinned) => action === 'pin'
|
|
53
|
-
? (pinned ? 'resume-menu-unpin' : 'resume-menu-pin')
|
|
54
|
-
: MENU_LABEL_KEYS[action];
|
|
55
|
-
/** A thrown value's message, for a notification that has to say something. */
|
|
56
|
-
function message(error) {
|
|
57
|
-
return error instanceof Error ? error.message : String(error);
|
|
58
|
-
}
|
|
59
|
-
/** Rendered width of a hint: `HintLine` strips the `**` emphasis markers. */
|
|
60
|
-
function hintWidth(text) {
|
|
61
|
-
return stringWidth(text.replace(/\*\*/gu, ''));
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* The widest hint that fits, cut only when even the shortest will not.
|
|
65
|
-
*
|
|
66
|
-
* A wrapped hint is worse than an abbreviated one in two ways: it eats rows
|
|
67
|
-
* the list needs, and — being the last region on screen — the part that falls
|
|
68
|
-
* off the bottom is its own tail, so the keys nobody can guess are exactly
|
|
69
|
-
* the ones that disappear.
|
|
70
|
-
*
|
|
71
|
-
* @param candidates - Variants from widest to narrowest.
|
|
72
|
-
* @param budget - Columns available to the row.
|
|
73
|
-
*/
|
|
74
|
-
function fitHint(candidates, budget) {
|
|
75
|
-
for (const candidate of candidates) {
|
|
76
|
-
if (hintWidth(candidate) <= budget)
|
|
77
|
-
return candidate;
|
|
78
|
-
}
|
|
79
|
-
return truncateWidth((candidates[candidates.length - 1] ?? '').replace(/\*\*/gu, ''), budget);
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* The session browser — `/resume` as a screen of its own.
|
|
83
|
-
*
|
|
84
|
-
* The old picker was a panel of eight titles and a timestamp, and the reason
|
|
85
|
-
* it could not be more than that was never layout: the data behind it was five
|
|
86
|
-
* fields wide. With every session now arriving classified, sized, dated and
|
|
87
|
-
* attributed, the surface that shows them can do the job a person actually
|
|
88
|
-
* came for — find one conversation among many.
|
|
89
|
-
*
|
|
90
|
-
* What that means concretely:
|
|
91
|
-
*
|
|
92
|
-
* - Working-directory scope is visible and selectable. Wide terminals keep a
|
|
93
|
-
* directory rail beside the sessions; narrow terminals drill into the same
|
|
94
|
-
* directory list without sacrificing title width.
|
|
95
|
-
* - Search is always live in the active list. There is no mode to enter;
|
|
96
|
-
* typing filters, because the list is the search results.
|
|
97
|
-
* - Delegated sub-agent runs are folded away by default and revealed under
|
|
98
|
-
* their parents on demand. They are not noise to be deleted — they are the
|
|
99
|
-
* model's own work, and it is worth being able to open one — but they are
|
|
100
|
-
* not what "resume a conversation" means, and there are five of them for
|
|
101
|
-
* every conversation.
|
|
102
|
-
* - Sessions that hold no conversation are never listed, only counted, with
|
|
103
|
-
* one action to clear them.
|
|
104
|
-
* - The preview shows the end of a session, so "is this the one I was in the
|
|
105
|
-
* middle of" is answerable without resuming it.
|
|
106
|
-
*
|
|
107
|
-
* Steady-state listings reuse a revision-keyed digest cache. The rare legacy
|
|
108
|
-
* log whose opening prompt sits beyond the cheap window is scanned
|
|
109
|
-
* progressively and cached, so it recovers a real title without making every
|
|
110
|
-
* browser open pay for full history.
|
|
111
|
-
*/
|
|
112
|
-
export function SessionBrowser({ channel, home, sameProject, onClose, }) {
|
|
113
|
-
const { columns, rows } = useTerminalSize();
|
|
114
|
-
const inset = React.useContext(PageInsetContext);
|
|
115
|
-
const isTerminalFocused = useTerminalFocus();
|
|
116
|
-
const [sessions, setSessions] = React.useState([]);
|
|
117
|
-
const [loaded, setLoaded] = React.useState(false);
|
|
118
|
-
const [filters, setFilters] = React.useState(DEFAULT_FILTERS);
|
|
119
|
-
const [level, setLevel] = React.useState('sessions');
|
|
120
|
-
const [selectedWorkspaceId, setSelectedWorkspaceId] = React.useState('current');
|
|
121
|
-
const [workspaceFocusId, setWorkspaceFocusId] = React.useState('current');
|
|
122
|
-
const [workspaceQuery, setWorkspaceQuery] = React.useState('');
|
|
123
|
-
const [scopeHovered, setScopeHovered] = React.useState(false);
|
|
124
|
-
// The cursor is a session ID, not a row index.
|
|
125
|
-
//
|
|
126
|
-
// Rows are reordered by almost everything the browser does: a rename touches
|
|
127
|
-
// MRU and moves its row to the top, a filter rebuilds the list, a delete
|
|
128
|
-
// removes one. An index survives none of those — it keeps pointing at a
|
|
129
|
-
// POSITION, so the cursor silently lands on a different session and the next
|
|
130
|
-
// Enter or ctrl+d acts on the wrong one. Tracking identity makes "the cursor
|
|
131
|
-
// stays on the session you were looking at" true by construction instead of
|
|
132
|
-
// something every mutation has to remember to restore.
|
|
133
|
-
const [focusId, setFocusId] = React.useState(undefined);
|
|
134
|
-
// The window start is a scroll anchor, not state: it is derived from the
|
|
135
|
-
// focus every render and only ever read back to keep a stationary cursor
|
|
136
|
-
// from re-shuffling the screen. Holding it in state would mean setting
|
|
137
|
-
// state during render to resolve a layout that render already knows.
|
|
138
|
-
const topRef = React.useRef(0);
|
|
139
|
-
const workspaceTopRef = React.useRef(0);
|
|
140
|
-
const [mode, setMode] = React.useState('list');
|
|
141
|
-
const [renameText, setRenameText] = React.useState('');
|
|
142
|
-
/** Hover state for the confirm rows (only one is ever mounted). */
|
|
143
|
-
const [confirmHovered, setConfirmHovered] = React.useState(false);
|
|
144
|
-
// The browser owns the whole screen, so it has to carry its own messages.
|
|
145
|
-
// `channel.notify` renders into the conversation — which is exactly what is
|
|
146
|
-
// NOT on screen right now — so a failed delete, a refused resume, or an
|
|
147
|
-
// unreadable listing would otherwise report itself to a hidden surface and
|
|
148
|
-
// look to the user like nothing happened at all.
|
|
149
|
-
const [notice, setNotice] = React.useState();
|
|
150
|
-
const [previewOpen, setPreviewOpen] = React.useState(false);
|
|
151
|
-
const [preview, setPreview] = React.useState([]);
|
|
152
|
-
const [previewLoading, setPreviewLoading] = React.useState(false);
|
|
153
|
-
/**
|
|
154
|
-
* Pinned session ids, from `~/.dsh-tui/session-pins.json`. Pins whose
|
|
155
|
-
* sessions no longer exist are inert: the view filters them against the
|
|
156
|
-
* live listing, and the file is left alone (lazy tolerance — a preference
|
|
157
|
-
* file is not an index and rewrites cost more than stale entries).
|
|
158
|
-
*/
|
|
159
|
-
const [pins, setPins] = React.useState(() => readSessionPins());
|
|
160
|
-
// Keep pin toggles synchronous across several keys delivered in one stdin
|
|
161
|
-
// chunk. The state render may lag, but the next command must see the set the
|
|
162
|
-
// previous command successfully persisted.
|
|
163
|
-
const pinsRef = React.useRef(pins);
|
|
164
|
-
pinsRef.current = pins;
|
|
165
|
-
/**
|
|
166
|
-
* The right-click session menu: which session it belongs to, where the
|
|
167
|
-
* pointer was (0-indexed screen coords, for anchoring), and which item
|
|
168
|
-
* the keyboard cursor is on. Undefined = no menu.
|
|
169
|
-
*/
|
|
170
|
-
const [menu, setMenu] = React.useState(undefined);
|
|
171
|
-
// React batches every parsed key from one stdin chunk. Lock actions in a
|
|
172
|
-
// ref so a repeated Enter cannot start the same async operation twice
|
|
173
|
-
// before the mode change is rendered.
|
|
174
|
-
const actionPendingRef = React.useRef(false);
|
|
175
|
-
// One clock per render pass: every relative time on screen must agree, and
|
|
176
|
-
// re-reading it per row would let two rows a millisecond apart round to
|
|
177
|
-
// different minutes.
|
|
178
|
-
const now = Date.now();
|
|
179
|
-
/**
|
|
180
|
-
* Say something, on screen and in the transcript.
|
|
181
|
-
*
|
|
182
|
-
* Both, deliberately: the line below the list is what the user can actually
|
|
183
|
-
* read right now, and the transcript entry is what they still have after
|
|
184
|
-
* they leave.
|
|
185
|
-
*/
|
|
186
|
-
const report = React.useCallback((text, tone) => {
|
|
187
|
-
setNotice({ text, tone });
|
|
188
|
-
channel.notify(text, tone === 'error' ? { color: 'error' } : {});
|
|
189
|
-
}, [channel]);
|
|
190
|
-
// Every await below is wrapped. The adapter's own paths are total functions
|
|
191
|
-
// that degrade rather than throw, but this is the UI: a rejection escaping
|
|
192
|
-
// an event handler here becomes an unhandled rejection with the terminal in
|
|
193
|
-
// raw mode and the alternate screen active — the worst possible place to
|
|
194
|
-
// find out a promise was not caught. A failure leaves the browser standing
|
|
195
|
-
// with whatever it already had.
|
|
196
|
-
const reload = React.useCallback(async () => {
|
|
197
|
-
try {
|
|
198
|
-
setSessions(await channel.listSessions());
|
|
199
|
-
}
|
|
200
|
-
catch (error) {
|
|
201
|
-
report(t('session-list-failed', { err: message(error) }), 'error');
|
|
202
|
-
}
|
|
203
|
-
finally {
|
|
204
|
-
setLoaded(true);
|
|
205
|
-
}
|
|
206
|
-
}, [channel, report]);
|
|
207
|
-
React.useEffect(() => {
|
|
208
|
-
void reload();
|
|
209
|
-
}, [reload]);
|
|
210
|
-
const browserContext = React.useMemo(() => ({
|
|
211
|
-
cwd: channel.cwd,
|
|
212
|
-
branch: channel.gitBranch,
|
|
213
|
-
currentId: channel.agentId,
|
|
214
|
-
sameProject,
|
|
215
|
-
}), [channel.cwd, channel.gitBranch, channel.agentId, sameProject]);
|
|
216
|
-
const workspaceGroups = React.useMemo(() => buildWorkspaceGroups(sessions, browserContext), [sessions, browserContext]);
|
|
217
|
-
const totalWorkspaceSessions = workspaceGroups.reduce((sum, group) => sum + group.count, 0);
|
|
218
|
-
const workspaceChoices = React.useMemo(() => {
|
|
219
|
-
return [
|
|
220
|
-
{ id: 'all', kind: 'all', count: totalWorkspaceSessions },
|
|
221
|
-
...workspaceGroups.map((workspace) => ({
|
|
222
|
-
id: workspace.id,
|
|
223
|
-
kind: 'workspace',
|
|
224
|
-
workspace,
|
|
225
|
-
})),
|
|
226
|
-
];
|
|
227
|
-
}, [workspaceGroups, totalWorkspaceSessions]);
|
|
228
|
-
const filteredWorkspaceChoices = React.useMemo(() => {
|
|
229
|
-
const needle = workspaceQuery.trim().toLowerCase();
|
|
230
|
-
if (needle === '')
|
|
231
|
-
return workspaceChoices;
|
|
232
|
-
return workspaceChoices.filter((choice) => {
|
|
233
|
-
if (choice.kind === 'all') {
|
|
234
|
-
return `${t('session-workspace-all')} ${t('session-scope-all')} ${t('session-workspace-all-detail', { n: choice.count })}`
|
|
235
|
-
.toLowerCase()
|
|
236
|
-
.includes(needle);
|
|
237
|
-
}
|
|
238
|
-
const state = choice.workspace.current
|
|
239
|
-
? t('session-workspace-current')
|
|
240
|
-
: choice.workspace.count === 0
|
|
241
|
-
? t('session-workspace-empty')
|
|
242
|
-
: '';
|
|
243
|
-
return `${projectName(choice.workspace.cwd)} ${formatProject(choice.workspace.cwd, home)} ${state}`
|
|
244
|
-
.toLowerCase()
|
|
245
|
-
.includes(needle);
|
|
246
|
-
});
|
|
247
|
-
}, [workspaceChoices, workspaceQuery, home]);
|
|
248
|
-
const selectedWorkspace = selectedWorkspaceId === 'all'
|
|
249
|
-
? undefined
|
|
250
|
-
: workspaceGroups.find(group => group.id === selectedWorkspaceId) ?? workspaceGroups[0];
|
|
251
|
-
React.useEffect(() => {
|
|
252
|
-
if (selectedWorkspaceId === 'all' || workspaceGroups.some(group => group.id === selectedWorkspaceId))
|
|
253
|
-
return;
|
|
254
|
-
setSelectedWorkspaceId('current');
|
|
255
|
-
setWorkspaceFocusId('current');
|
|
256
|
-
setFilters(current => ({ ...current, allProjects: false }));
|
|
257
|
-
}, [selectedWorkspaceId, workspaceGroups]);
|
|
258
|
-
const scopeCwd = selectedWorkspace?.cwd ?? channel.cwd;
|
|
259
|
-
const scopeSameProject = React.useMemo(() => selectedWorkspace?.current !== false
|
|
260
|
-
? sameProject
|
|
261
|
-
: (left, right) => normalizeWorkspaceCwd(left) === normalizeWorkspaceCwd(right), [selectedWorkspace?.id, selectedWorkspace?.current, sameProject]);
|
|
262
|
-
const view = React.useMemo(() => buildView(sessions, filters, {
|
|
263
|
-
cwd: scopeCwd,
|
|
264
|
-
branch: channel.gitBranch,
|
|
265
|
-
currentId: channel.agentId,
|
|
266
|
-
sameProject: scopeSameProject,
|
|
267
|
-
}, pins), [sessions, filters, scopeCwd, channel.gitBranch, channel.agentId, scopeSameProject, pins]);
|
|
268
|
-
// Resolve identity to a position once per render. A cursor whose session is
|
|
269
|
-
// gone — deleted, filtered out, or never there — falls to the first
|
|
270
|
-
// selectable row rather than to nothing, so the list is never unusable.
|
|
271
|
-
const focus = React.useMemo(() => {
|
|
272
|
-
const byId = view.rows.findIndex(row => row.kind === 'session' && row.session.id === focusId);
|
|
273
|
-
return byId >= 0 ? byId : Math.max(0, seekSelectable(view.rows, 0, 1));
|
|
274
|
-
}, [view.rows, focusId]);
|
|
275
|
-
const focused = sessionAt(view.rows, focus);
|
|
276
|
-
/** The session the open menu acts on; undefined once it leaves the view. */
|
|
277
|
-
const menuTarget = React.useMemo(() => {
|
|
278
|
-
if (menu === undefined)
|
|
279
|
-
return undefined;
|
|
280
|
-
for (const row of view.rows) {
|
|
281
|
-
if (row.kind === 'session' && row.session.id === menu.sessionId)
|
|
282
|
-
return row.session;
|
|
283
|
-
}
|
|
284
|
-
return undefined;
|
|
285
|
-
}, [view.rows, menu]);
|
|
286
|
-
// A menu whose session vanished (filtered out, deleted by another path)
|
|
287
|
-
// must not linger invisible but still capture keys.
|
|
288
|
-
React.useEffect(() => {
|
|
289
|
-
if (menu !== undefined && menuTarget === undefined)
|
|
290
|
-
closeMenu();
|
|
291
|
-
}, [menu, menuTarget]);
|
|
292
|
-
const workspaceFocus = React.useMemo(() => {
|
|
293
|
-
const byId = filteredWorkspaceChoices.findIndex(choice => choice.id === workspaceFocusId);
|
|
294
|
-
if (byId >= 0)
|
|
295
|
-
return byId;
|
|
296
|
-
const selected = filteredWorkspaceChoices.findIndex(choice => choice.id === selectedWorkspaceId);
|
|
297
|
-
return Math.max(0, selected);
|
|
298
|
-
}, [filteredWorkspaceChoices, workspaceFocusId, selectedWorkspaceId]);
|
|
299
|
-
const focusedWorkspace = filteredWorkspaceChoices[workspaceFocus];
|
|
300
|
-
/**
|
|
301
|
-
* Resume a session (Enter and row-click share this path). The screen closes
|
|
302
|
-
* only once the resume actually happened. Closing first and letting a
|
|
303
|
-
* refusal fall through to a notification would send that explanation to the
|
|
304
|
-
* conversation the user is not looking at, and leave them staring at an
|
|
305
|
-
* unchanged transcript wondering what Enter did. `resumeTo` reports its own
|
|
306
|
-
* reasons; this only has to stay put.
|
|
307
|
-
*/
|
|
308
|
-
const resumeSession = (target) => {
|
|
309
|
-
runAction(async () => {
|
|
310
|
-
try {
|
|
311
|
-
const result = await channel.resumeTo(target.id);
|
|
312
|
-
if (result.ok) {
|
|
313
|
-
channel.notify(t('resume-resumed'));
|
|
314
|
-
onClose();
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
if (result.reason === 'cancelled')
|
|
318
|
-
return;
|
|
319
|
-
const text = result.reason === 'working'
|
|
320
|
-
? t('resume-while-working')
|
|
321
|
-
: result.reason === 'unavailable'
|
|
322
|
-
? t('resume-unavailable')
|
|
323
|
-
: t('session-resume-failed', { err: result.error });
|
|
324
|
-
setNotice({ text, tone: 'error' });
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
catch (error) {
|
|
328
|
-
setNotice({ text: t('session-resume-failed', { err: message(error) }), tone: 'error' });
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
};
|
|
332
|
-
const chooseWorkspace = (choice) => {
|
|
333
|
-
if (mode !== 'list' || actionPendingRef.current)
|
|
334
|
-
return;
|
|
335
|
-
closeMenu();
|
|
336
|
-
setSelectedWorkspaceId(choice.id);
|
|
337
|
-
setWorkspaceFocusId(choice.id);
|
|
338
|
-
setFilters(current => ({ ...current, query: '', allProjects: choice.kind === 'all' }));
|
|
339
|
-
setFocusId(undefined);
|
|
340
|
-
setPreviewOpen(false);
|
|
341
|
-
setPreview([]);
|
|
342
|
-
topRef.current = 0;
|
|
343
|
-
setLevel('sessions');
|
|
344
|
-
};
|
|
345
|
-
const openWorkspaceMenu = () => {
|
|
346
|
-
if (mode !== 'list' || actionPendingRef.current)
|
|
347
|
-
return;
|
|
348
|
-
closeMenu();
|
|
349
|
-
setScopeHovered(false);
|
|
350
|
-
setWorkspaceFocusId(selectedWorkspaceId);
|
|
351
|
-
setLevel('workspaces');
|
|
352
|
-
};
|
|
353
|
-
// Where the cursor is RIGHT NOW, including moves made earlier in this same
|
|
354
|
-
// tick. A held arrow key (or a paste) delivers several key events from one
|
|
355
|
-
// stdin chunk, and every one of them runs before React re-renders — so a
|
|
356
|
-
// handler that read the position from the render closure would compute all
|
|
357
|
-
// of them from the same starting row and keep only the last, silently
|
|
358
|
-
// dropping every move but one.
|
|
359
|
-
const focusRef = React.useRef(focus);
|
|
360
|
-
focusRef.current = focus;
|
|
361
|
-
/** Move the cursor by rows, then store the session it landed on. */
|
|
362
|
-
const step = (by, times = 1) => {
|
|
363
|
-
let next = focusRef.current;
|
|
364
|
-
for (let taken = 0; taken < times; taken++)
|
|
365
|
-
next = moveSelection(view.rows, next, by);
|
|
366
|
-
focusRef.current = next;
|
|
367
|
-
const landed = sessionAt(view.rows, next);
|
|
368
|
-
if (landed !== undefined)
|
|
369
|
-
setFocusId(landed.id);
|
|
370
|
-
};
|
|
371
|
-
const workspaceFocusRef = React.useRef(workspaceFocus);
|
|
372
|
-
workspaceFocusRef.current = workspaceFocus;
|
|
373
|
-
const stepWorkspace = (by, times = 1) => {
|
|
374
|
-
if (filteredWorkspaceChoices.length === 0)
|
|
375
|
-
return;
|
|
376
|
-
let next = workspaceFocusRef.current;
|
|
377
|
-
for (let taken = 0; taken < times; taken++) {
|
|
378
|
-
next = (next + by + filteredWorkspaceChoices.length) % filteredWorkspaceChoices.length;
|
|
379
|
-
}
|
|
380
|
-
workspaceFocusRef.current = next;
|
|
381
|
-
const landed = filteredWorkspaceChoices[next];
|
|
382
|
-
if (landed !== undefined)
|
|
383
|
-
setWorkspaceFocusId(landed.id);
|
|
384
|
-
};
|
|
385
|
-
// The menu cursor has the same problem the session cursor has: several
|
|
386
|
-
// keys from one stdin chunk are all handled before React re-renders, so
|
|
387
|
-
// a ↓ then Enter in the same chunk would read the pre-↓ item from the
|
|
388
|
-
// closure. Move the item through a ref, like `step`/`stepWorkspace`.
|
|
389
|
-
const menuRef = React.useRef(menu);
|
|
390
|
-
menuRef.current = menu;
|
|
391
|
-
const stepMenuItem = (by) => {
|
|
392
|
-
const current = menuRef.current;
|
|
393
|
-
if (current === undefined)
|
|
394
|
-
return;
|
|
395
|
-
const next = { ...current, item: (current.item + by + MENU_ACTIONS.length) % MENU_ACTIONS.length };
|
|
396
|
-
menuRef.current = next;
|
|
397
|
-
setMenu(next);
|
|
398
|
-
};
|
|
399
|
-
/**
|
|
400
|
-
* Dismiss the context menu. State and ref are cleared TOGETHER: the
|
|
401
|
-
* keyboard branch gates on menuRef, and a mouse dismissal followed by a
|
|
402
|
-
* keystroke in the same stdin chunk must not re-activate the stale menu.
|
|
403
|
-
*/
|
|
404
|
-
const closeMenu = () => {
|
|
405
|
-
menuRef.current = undefined;
|
|
406
|
-
setMenu(undefined);
|
|
407
|
-
};
|
|
408
|
-
const handleSessionWheel = (event) => {
|
|
409
|
-
if (mode !== 'list' || actionPendingRef.current)
|
|
410
|
-
return;
|
|
411
|
-
// 菜单开着时滚轮只是关掉菜单,不滚动列表——行是惰性的,滚轮
|
|
412
|
-
// 不该在用户还没看清菜单时把目标行换走。
|
|
413
|
-
if (menu !== undefined) {
|
|
414
|
-
closeMenu();
|
|
415
|
-
return;
|
|
416
|
-
}
|
|
417
|
-
setLevel('sessions');
|
|
418
|
-
step(event.deltaY >= 0 ? 1 : -1);
|
|
419
|
-
};
|
|
420
|
-
const handleWorkspaceWheel = (event) => {
|
|
421
|
-
if (mode !== 'list' || actionPendingRef.current)
|
|
422
|
-
return;
|
|
423
|
-
if (menu !== undefined) {
|
|
424
|
-
closeMenu();
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
setLevel('workspaces');
|
|
428
|
-
stepWorkspace(event.deltaY >= 0 ? 1 : -1);
|
|
429
|
-
};
|
|
430
|
-
// Preview follows the cursor. Keyed on the id so arrowing past a row does
|
|
431
|
-
// not refetch the one it came from, and guarded on unmount so a slow read
|
|
432
|
-
// landing after the cursor moved cannot overwrite a newer preview.
|
|
433
|
-
React.useEffect(() => {
|
|
434
|
-
if (!previewOpen || focused === undefined)
|
|
435
|
-
return;
|
|
436
|
-
let live = true;
|
|
437
|
-
setPreviewLoading(true);
|
|
438
|
-
void channel
|
|
439
|
-
.previewSession(focused.id)
|
|
440
|
-
.then(entries => (live ? entries : undefined))
|
|
441
|
-
.catch(() => (live ? [] : undefined))
|
|
442
|
-
.then((entries) => {
|
|
443
|
-
if (entries === undefined)
|
|
444
|
-
return;
|
|
445
|
-
setPreview(entries);
|
|
446
|
-
setPreviewLoading(false);
|
|
447
|
-
});
|
|
448
|
-
return () => {
|
|
449
|
-
live = false;
|
|
450
|
-
};
|
|
451
|
-
}, [channel, previewOpen, focused?.id]);
|
|
452
|
-
// Wide terminals keep a directory rail visible. Narrow terminals drill into
|
|
453
|
-
// the same list as a page, so classification stays explicit without leaving
|
|
454
|
-
// too little width for two-line session rows.
|
|
455
|
-
const compactChrome = rows < 10;
|
|
456
|
-
const mandatoryLines = compactChrome ? COMPACT_MANDATORY_LINES : REGULAR_MANDATORY_LINES;
|
|
457
|
-
const workspaceRail = columns >= WORKSPACE_RAIL_MIN_COLUMNS;
|
|
458
|
-
const workspaceWidth = workspaceRail
|
|
459
|
-
? Math.min(34, Math.max(26, Math.floor(columns * 0.24)))
|
|
460
|
-
: columns;
|
|
461
|
-
const workspaceGap = workspaceRail ? 1 : 0;
|
|
462
|
-
const sessionAreaWidth = Math.max(20, columns - (workspaceRail ? workspaceWidth + workspaceGap : 0));
|
|
463
|
-
const workspaceAreaVisible = workspaceRail || level === 'workspaces';
|
|
464
|
-
const sessionAreaVisible = workspaceRail || level === 'sessions';
|
|
465
|
-
// Preview joins the session list only when the SESSION area itself is wide;
|
|
466
|
-
// otherwise it replaces that list while the directory rail (if any) remains.
|
|
467
|
-
const canPreview = focused !== undefined;
|
|
468
|
-
const splitPreview = previewOpen && canPreview && sessionAreaVisible && sessionAreaWidth >= SPLIT_MIN_COLUMNS;
|
|
469
|
-
const soloPreview = previewOpen && canPreview && sessionAreaVisible && !splitPreview;
|
|
470
|
-
const previewWidth = splitPreview
|
|
471
|
-
? Math.min(56, Math.floor(sessionAreaWidth * 0.42))
|
|
472
|
-
: sessionAreaWidth;
|
|
473
|
-
const listWidth = Math.max(20, sessionAreaWidth - (splitPreview ? previewWidth : 0));
|
|
474
|
-
// Height is distributed, never assumed. The permanent notice slot prevents
|
|
475
|
-
// mutation/error feedback from shifting the list under the cursor.
|
|
476
|
-
const extraLines = mode === 'list' ? 0 : 1;
|
|
477
|
-
// Decoration yields before the first complete two-line selectable row.
|
|
478
|
-
const ruleBudget = Math.max(0, Math.min(RULE_PRIORITY.length, rows - mandatoryLines - extraLines - WORKSPACE_ROW_HEIGHT));
|
|
479
|
-
const rules = new Set(RULE_PRIORITY.slice(0, ruleBudget));
|
|
480
|
-
const listHeight = Math.max(0, rows - mandatoryLines - extraLines - rules.size);
|
|
481
|
-
const windowTop = anchorTop(view.rows, focus, listHeight, topRef.current);
|
|
482
|
-
topRef.current = windowTop;
|
|
483
|
-
const visible = view.rows.slice(windowTop, windowEnd(view.rows, windowTop, listHeight));
|
|
484
|
-
const workspaceCapacity = Math.max(0, Math.floor(listHeight / WORKSPACE_ROW_HEIGHT));
|
|
485
|
-
let workspaceTop = Math.min(Math.max(0, workspaceTopRef.current), Math.max(0, filteredWorkspaceChoices.length - workspaceCapacity));
|
|
486
|
-
if (workspaceFocus < workspaceTop)
|
|
487
|
-
workspaceTop = workspaceFocus;
|
|
488
|
-
if (workspaceCapacity > 0 && workspaceFocus >= workspaceTop + workspaceCapacity) {
|
|
489
|
-
workspaceTop = workspaceFocus - workspaceCapacity + 1;
|
|
490
|
-
}
|
|
491
|
-
workspaceTopRef.current = workspaceTop;
|
|
492
|
-
const visibleWorkspaces = workspaceCapacity === 0
|
|
493
|
-
? []
|
|
494
|
-
: filteredWorkspaceChoices.slice(workspaceTop, workspaceTop + workspaceCapacity);
|
|
495
|
-
/**
|
|
496
|
-
* Change the view.
|
|
497
|
-
*
|
|
498
|
-
* Takes a function of the CURRENT filters rather than a ready-made patch,
|
|
499
|
-
* for the same reason the cursor keeps a ref: several key events can be
|
|
500
|
-
* handled before React re-renders, and a patch built from the render
|
|
501
|
-
* closure would compute each of them from the same starting filters — two
|
|
502
|
-
* toggles in one chunk cancelling to one, or typed characters overwriting
|
|
503
|
-
* each other instead of accumulating.
|
|
504
|
-
*/
|
|
505
|
-
const applyFilters = (update) => {
|
|
506
|
-
setFilters(current => ({ ...current, ...update(current) }));
|
|
507
|
-
topRef.current = 0;
|
|
508
|
-
};
|
|
509
|
-
const runAction = (action) => {
|
|
510
|
-
if (actionPendingRef.current)
|
|
511
|
-
return;
|
|
512
|
-
actionPendingRef.current = true;
|
|
513
|
-
void action().finally(() => {
|
|
514
|
-
actionPendingRef.current = false;
|
|
515
|
-
});
|
|
516
|
-
};
|
|
517
|
-
/** Run one mutation, report it, and re-list — reporting a failure either way. */
|
|
518
|
-
const mutate = (action, done, failed) => {
|
|
519
|
-
runAction(async () => {
|
|
520
|
-
let ok = false;
|
|
521
|
-
let reason;
|
|
522
|
-
try {
|
|
523
|
-
ok = await action();
|
|
524
|
-
}
|
|
525
|
-
catch (error) {
|
|
526
|
-
reason = message(error);
|
|
527
|
-
}
|
|
528
|
-
report(ok ? done : reason === undefined ? failed : `${failed} · ${reason}`, ok ? 'info' : 'error');
|
|
529
|
-
await reload();
|
|
530
|
-
});
|
|
531
|
-
};
|
|
532
|
-
/** Persist one pin mutation and synchronously adopt the returned full set. */
|
|
533
|
-
const persistPin = (id, pinned) => {
|
|
534
|
-
const result = setSessionPinned(id, pinned);
|
|
535
|
-
if (!result.ok)
|
|
536
|
-
return false;
|
|
537
|
-
pinsRef.current = result.pins;
|
|
538
|
-
setPins(result.pins);
|
|
539
|
-
return true;
|
|
540
|
-
};
|
|
541
|
-
/**
|
|
542
|
-
* Pin or unpin one session. A pure view preference: no reload needed, the
|
|
543
|
-
* pinned group is derived from the listing already on screen. Persistence
|
|
544
|
-
* failure is visible and never paints a star that will disappear on restart.
|
|
545
|
-
*/
|
|
546
|
-
const togglePin = (target) => {
|
|
547
|
-
if (mode !== 'list' || actionPendingRef.current)
|
|
548
|
-
return;
|
|
549
|
-
closeMenu();
|
|
550
|
-
const pinning = !pinsRef.current.has(target.id);
|
|
551
|
-
if (!persistPin(target.id, pinning)) {
|
|
552
|
-
report(t('resume-pin-save-failed'), 'error');
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
report(t(pinning ? 'resume-pinned' : 'resume-unpinned', { name: target.title.text }), 'info');
|
|
556
|
-
};
|
|
557
|
-
const runDelete = (target) => mutate(async () => {
|
|
558
|
-
const ok = await channel.deleteSession(target.id);
|
|
559
|
-
// A deleted session's pin must not linger into the next launch's
|
|
560
|
-
// pinned group (where its row would then be silently missing).
|
|
561
|
-
if (ok && pinsRef.current.has(target.id)) {
|
|
562
|
-
// Failure is harmless here: the deleted id is lazily invisible, and
|
|
563
|
-
// a later pin mutation retries from the persisted set.
|
|
564
|
-
persistPin(target.id, false);
|
|
565
|
-
}
|
|
566
|
-
return ok;
|
|
567
|
-
}, t('resume-deleted', { name: target.title.text }), t('resume-delete-failed', { name: target.title.text }));
|
|
568
|
-
const runRename = (target, title) => mutate(() => channel.renameSessionTo(target.id, title), t('rename-done', { title }), t('resume-rename-failed', { name: target.title.text }));
|
|
569
|
-
const runClean = () => {
|
|
570
|
-
// Snapshot the ids before any await: the view is rebuilt by the reload
|
|
571
|
-
// below, and deleting from a list that moved under us would be a
|
|
572
|
-
// destructive action aimed at whatever happens to be there now.
|
|
573
|
-
const ids = [...view.emptyIds];
|
|
574
|
-
runAction(async () => {
|
|
575
|
-
let removed = 0;
|
|
576
|
-
for (const id of ids) {
|
|
577
|
-
try {
|
|
578
|
-
if (await channel.deleteSession(id))
|
|
579
|
-
removed += 1;
|
|
580
|
-
}
|
|
581
|
-
catch {
|
|
582
|
-
// One unremovable log must not abandon the rest of the sweep.
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
report(t('session-cleaned', { n: removed }), 'info');
|
|
586
|
-
await reload();
|
|
587
|
-
});
|
|
588
|
-
};
|
|
589
|
-
/**
|
|
590
|
-
* Run the action the menu cursor points at, then close the menu. Shares
|
|
591
|
-
* the keyboard paths: open = Enter, rename = Ctrl+R, delete = Ctrl+D.
|
|
592
|
-
*/
|
|
593
|
-
const activateMenu = (target, item) => {
|
|
594
|
-
closeMenu();
|
|
595
|
-
const action = MENU_ACTIONS[item];
|
|
596
|
-
if (action === 'open') {
|
|
597
|
-
resumeSession(target);
|
|
598
|
-
}
|
|
599
|
-
else if (action === 'pin') {
|
|
600
|
-
togglePin(target);
|
|
601
|
-
}
|
|
602
|
-
else if (action === 'rename') {
|
|
603
|
-
setRenameText(target.title.text);
|
|
604
|
-
setMode('rename');
|
|
605
|
-
}
|
|
606
|
-
else {
|
|
607
|
-
setMode('confirm-delete');
|
|
608
|
-
}
|
|
609
|
-
};
|
|
610
|
-
useInput((input, key) => {
|
|
611
|
-
if (actionPendingRef.current)
|
|
612
|
-
return;
|
|
613
|
-
// A notice describes what the LAST action did; the next keystroke makes it
|
|
614
|
-
// stale, so it goes as soon as the user acts again.
|
|
615
|
-
if (notice !== undefined)
|
|
616
|
-
setNotice(undefined);
|
|
617
|
-
// The context menu is modal like the confirmations: ↑/↓/Enter drive it,
|
|
618
|
-
// Esc dismisses it, and any other key dismisses it without acting
|
|
619
|
-
// (DOM-menu semantics — a stray keystroke should not also type into the
|
|
620
|
-
// search box behind a menu the user has not dismissed). The gate reads
|
|
621
|
-
// the REF so a right-click and a keystroke in the same stdin chunk (the
|
|
622
|
-
// click handler sets the ref before React renders) behave like the
|
|
623
|
-
// separate chunks they visually are.
|
|
624
|
-
if (menuRef.current !== undefined) {
|
|
625
|
-
if (key.upArrow) {
|
|
626
|
-
stepMenuItem(-1);
|
|
627
|
-
}
|
|
628
|
-
else if (key.downArrow) {
|
|
629
|
-
stepMenuItem(1);
|
|
630
|
-
}
|
|
631
|
-
else if (isPlainReturn(key)) {
|
|
632
|
-
const current = menuRef.current;
|
|
633
|
-
if (current !== undefined && menuTarget !== undefined)
|
|
634
|
-
activateMenu(menuTarget, current.item);
|
|
635
|
-
}
|
|
636
|
-
else {
|
|
637
|
-
closeMenu();
|
|
638
|
-
}
|
|
639
|
-
return;
|
|
640
|
-
}
|
|
641
|
-
if (mode === 'confirm-delete') {
|
|
642
|
-
if (isPlainReturn(key)) {
|
|
643
|
-
setMode('list');
|
|
644
|
-
if (focused !== undefined)
|
|
645
|
-
runDelete(focused);
|
|
646
|
-
}
|
|
647
|
-
else if (key.escape) {
|
|
648
|
-
setMode('list');
|
|
649
|
-
}
|
|
650
|
-
return;
|
|
651
|
-
}
|
|
652
|
-
if (mode === 'confirm-clean') {
|
|
653
|
-
if (isPlainReturn(key)) {
|
|
654
|
-
setMode('list');
|
|
655
|
-
runClean();
|
|
656
|
-
}
|
|
657
|
-
else if (key.escape) {
|
|
658
|
-
setMode('list');
|
|
659
|
-
}
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
if (mode === 'rename') {
|
|
663
|
-
if (isPlainReturn(key)) {
|
|
664
|
-
setMode('list');
|
|
665
|
-
const title = renameText.trim();
|
|
666
|
-
if (focused !== undefined && title.length > 0)
|
|
667
|
-
runRename(focused, title);
|
|
668
|
-
}
|
|
669
|
-
else if (key.escape) {
|
|
670
|
-
setMode('list');
|
|
671
|
-
}
|
|
672
|
-
else if (key.backspace || key.delete) {
|
|
673
|
-
setRenameText(text => text.slice(0, -1));
|
|
674
|
-
}
|
|
675
|
-
else if (!isMod(key) && !key.meta && input) {
|
|
676
|
-
setRenameText(text => text + input.replace(/[\r\n]+/g, ' '));
|
|
677
|
-
}
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
if (level === 'workspaces') {
|
|
681
|
-
if (key.upArrow || key.wheelUp) {
|
|
682
|
-
stepWorkspace(-1);
|
|
683
|
-
}
|
|
684
|
-
else if (key.downArrow || key.wheelDown) {
|
|
685
|
-
stepWorkspace(1);
|
|
686
|
-
}
|
|
687
|
-
else if (key.pageUp || key.pageDown) {
|
|
688
|
-
stepWorkspace(key.pageDown ? 1 : -1, Math.max(1, workspaceCapacity));
|
|
689
|
-
}
|
|
690
|
-
else if (isPlainReturn(key) || key.rightArrow) {
|
|
691
|
-
if (focusedWorkspace !== undefined)
|
|
692
|
-
chooseWorkspace(focusedWorkspace);
|
|
693
|
-
}
|
|
694
|
-
else if (key.escape || key.leftArrow) {
|
|
695
|
-
if (workspaceQuery.length > 0) {
|
|
696
|
-
setWorkspaceQuery('');
|
|
697
|
-
workspaceTopRef.current = 0;
|
|
698
|
-
}
|
|
699
|
-
else {
|
|
700
|
-
setLevel('sessions');
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
else if (isMod(key) && input === 'a') {
|
|
704
|
-
const all = workspaceChoices[0];
|
|
705
|
-
if (all !== undefined)
|
|
706
|
-
chooseWorkspace(all);
|
|
707
|
-
}
|
|
708
|
-
else if (key.backspace || key.delete) {
|
|
709
|
-
setWorkspaceQuery(query => query.slice(0, -1));
|
|
710
|
-
workspaceTopRef.current = 0;
|
|
711
|
-
}
|
|
712
|
-
else if (!isMod(key) && !key.meta && !key.super && input && !key.return) {
|
|
713
|
-
const typed = input.replace(/\p{Cc}/gu, '');
|
|
714
|
-
if (typed.length > 0) {
|
|
715
|
-
setWorkspaceQuery(query => query + typed);
|
|
716
|
-
workspaceTopRef.current = 0;
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
return;
|
|
720
|
-
}
|
|
721
|
-
if (key.leftArrow) {
|
|
722
|
-
openWorkspaceMenu();
|
|
723
|
-
}
|
|
724
|
-
else if (key.upArrow) {
|
|
725
|
-
step(-1);
|
|
726
|
-
}
|
|
727
|
-
else if (key.downArrow) {
|
|
728
|
-
step(1);
|
|
729
|
-
}
|
|
730
|
-
else if (key.wheelUp) {
|
|
731
|
-
// 滚轮在列表上:与 ↑ 同路径——焦点跟随窗口下移焦点即滚动
|
|
732
|
-
step(-1);
|
|
733
|
-
}
|
|
734
|
-
else if (key.wheelDown) {
|
|
735
|
-
step(1);
|
|
736
|
-
}
|
|
737
|
-
else if (key.pageUp || key.pageDown) {
|
|
738
|
-
// A page is "as many rows as the window holds", taken as repeated single
|
|
739
|
-
// steps so it lands on a selectable row like every other move.
|
|
740
|
-
step(key.pageDown ? 1 : -1, Math.max(1, Math.floor(listHeight / 2)));
|
|
741
|
-
}
|
|
742
|
-
else if (isPlainReturn(key)) {
|
|
743
|
-
if (focused === undefined)
|
|
744
|
-
return;
|
|
745
|
-
resumeSession(focused);
|
|
746
|
-
}
|
|
747
|
-
else if (key.escape) {
|
|
748
|
-
// Esc backs out one layer at a time: a live query first, the screen
|
|
749
|
-
// second. Closing on the first Esc would discard a search the user is
|
|
750
|
-
// still refining.
|
|
751
|
-
if (filters.query.length > 0)
|
|
752
|
-
applyFilters(() => ({ query: '' }));
|
|
753
|
-
else if (previewOpen)
|
|
754
|
-
setPreviewOpen(false);
|
|
755
|
-
else
|
|
756
|
-
onClose();
|
|
757
|
-
}
|
|
758
|
-
else if (key.tab) {
|
|
759
|
-
if (focused !== undefined)
|
|
760
|
-
setPreviewOpen(open => !open);
|
|
761
|
-
}
|
|
762
|
-
else if (isMod(key) && input === 'a') {
|
|
763
|
-
const target = filters.allProjects
|
|
764
|
-
? workspaceChoices.find(choice => choice.id === 'current')
|
|
765
|
-
: workspaceChoices[0];
|
|
766
|
-
if (target !== undefined)
|
|
767
|
-
chooseWorkspace(target);
|
|
768
|
-
}
|
|
769
|
-
else if (isMod(key) && input === 'b') {
|
|
770
|
-
applyFilters(current => ({ branchOnly: !current.branchOnly }));
|
|
771
|
-
}
|
|
772
|
-
else if (isMod(key) && input === 's') {
|
|
773
|
-
applyFilters(current => ({ showSubagents: !current.showSubagents }));
|
|
774
|
-
}
|
|
775
|
-
else if (isMod(key) && input === 'r' && focused !== undefined) {
|
|
776
|
-
setRenameText(focused.title.text);
|
|
777
|
-
setMode('rename');
|
|
778
|
-
}
|
|
779
|
-
else if (isMod(key) && input === 'p') {
|
|
780
|
-
// Resolve through focusRef, not the render closure: Down + Ctrl+P may
|
|
781
|
-
// arrive in one stdin chunk, and the pin must follow the moved cursor.
|
|
782
|
-
// Plain `p` remains search text like every unbound character.
|
|
783
|
-
const target = sessionAt(view.rows, focusRef.current);
|
|
784
|
-
if (target !== undefined)
|
|
785
|
-
togglePin(target);
|
|
786
|
-
}
|
|
787
|
-
else if (isMod(key) && input === 'd' && focused !== undefined) {
|
|
788
|
-
setMode('confirm-delete');
|
|
789
|
-
}
|
|
790
|
-
else if (isMod(key) && input === 'x' && view.emptyCount > 0) {
|
|
791
|
-
setMode('confirm-clean');
|
|
792
|
-
}
|
|
793
|
-
else if (key.backspace || key.delete) {
|
|
794
|
-
applyFilters(current => ({ query: current.query.slice(0, -1) }));
|
|
795
|
-
}
|
|
796
|
-
else if (!isMod(key) && !key.meta && !key.super && input && !key.return) {
|
|
797
|
-
// Only real characters reach the query. Anything else the terminal
|
|
798
|
-
// delivers — an unbound control byte, a chord this screen does not
|
|
799
|
-
// claim, the newlines inside a paste — would otherwise be typed into
|
|
800
|
-
// the search box invisibly, leaving a filter that matches nothing for
|
|
801
|
-
// no reason the user can see.
|
|
802
|
-
const typed = input.replace(/\p{Cc}/gu, '');
|
|
803
|
-
if (typed.length > 0)
|
|
804
|
-
applyFilters(current => ({ query: current.query + typed }));
|
|
805
|
-
}
|
|
806
|
-
});
|
|
807
|
-
const counts = [];
|
|
808
|
-
if (level === 'workspaces') {
|
|
809
|
-
counts.push(t('session-workspace-project-count', { n: workspaceGroups.length }));
|
|
810
|
-
counts.push(t('session-count-shown', { n: totalWorkspaceSessions }));
|
|
811
|
-
}
|
|
812
|
-
else {
|
|
813
|
-
counts.push(t('session-count-shown', { n: view.shown }));
|
|
814
|
-
if (view.hiddenSubagents > 0)
|
|
815
|
-
counts.push(t('session-count-subagents', { n: view.hiddenSubagents }));
|
|
816
|
-
if (view.emptyCount > 0)
|
|
817
|
-
counts.push(t('session-count-empty', { n: view.emptyCount }));
|
|
818
|
-
}
|
|
819
|
-
// Every chrome row is pre-measured in terminal cells; flex truncation at an
|
|
820
|
-
// exact fit can otherwise wrap one localized character and shift the footer.
|
|
821
|
-
const heading = ` ↻ ${t('resume-title')}`;
|
|
822
|
-
const header = spreadRow(heading, counts.join(' · '), Math.max(0, columns - 1));
|
|
823
|
-
const scope = filters.allProjects
|
|
824
|
-
? t('session-scope-all')
|
|
825
|
-
: formatProject(selectedWorkspace?.cwd ?? channel.cwd, home);
|
|
826
|
-
// The scope and search share one four-row card (border + two content rows),
|
|
827
|
-
// making directory classification visible without stacking two big panels.
|
|
828
|
-
const inputBudget = Math.max(0, columns - (compactChrome ? 1 : 4));
|
|
829
|
-
const scopeLeft = level === 'workspaces'
|
|
830
|
-
? `▣ ${t('session-workspace-select-title')}`
|
|
831
|
-
: `▣ ${t('session-workspace-scope')} ${scope}`;
|
|
832
|
-
const scopeRight = level === 'workspaces'
|
|
833
|
-
? t('session-workspace-project-count', { n: workspaceGroups.length })
|
|
834
|
-
: t('session-workspace-switch');
|
|
835
|
-
const scopeRow = spreadRow(scopeLeft, scopeRight, inputBudget);
|
|
836
|
-
const activeQuery = level === 'workspaces' ? workspaceQuery : filters.query;
|
|
837
|
-
const searchPlaceholder = level === 'workspaces'
|
|
838
|
-
? t('session-workspace-search-placeholder')
|
|
839
|
-
: t('session-search-placeholder', { scope });
|
|
840
|
-
const hint = mode === 'confirm-delete' || mode === 'confirm-clean'
|
|
841
|
-
? fitHint([t('resume-hint-delete')], inputBudget)
|
|
842
|
-
: mode === 'rename'
|
|
843
|
-
? fitHint([t('resume-hint-rename')], inputBudget)
|
|
844
|
-
: level === 'workspaces'
|
|
845
|
-
? fitHint([
|
|
846
|
-
t('session-hint-workspaces', { mod: modLabel }),
|
|
847
|
-
t('session-hint-workspaces-short'),
|
|
848
|
-
], inputBudget)
|
|
849
|
-
: fitHint([
|
|
850
|
-
t('session-hint-list', {
|
|
851
|
-
mod: modLabel,
|
|
852
|
-
projects: filters.allProjects ? t('session-toggle-on') : t('session-toggle-off'),
|
|
853
|
-
runs: filters.showSubagents ? t('session-toggle-on') : t('session-toggle-off'),
|
|
854
|
-
}),
|
|
855
|
-
t('session-hint-list-mid', { mod: modLabel }),
|
|
856
|
-
t('session-hint-list-short', { mod: modLabel }),
|
|
857
|
-
], inputBudget);
|
|
858
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, onClick: menu !== undefined ? closeMenu : undefined, children: [_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: "remember", bold: true, children: header.left }), _jsx(Text, { dimColor: true, children: `${' '.repeat(header.gap)}${header.right}` })] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, borderStyle: compactChrome ? undefined : 'round', borderColor: !compactChrome && mode === 'list' ? 'permission' : undefined, borderDimColor: !compactChrome && mode !== 'list', paddingX: compactChrome ? 0 : 1, children: [_jsxs(Box, { height: 1, flexShrink: 0, overflow: "hidden", onClick: level === 'sessions' && mode === 'list' ? openWorkspaceMenu : undefined, onMouseEnter: level === 'sessions' && mode === 'list' ? () => setScopeHovered(true) : undefined, onMouseLeave: level === 'sessions' && mode === 'list' ? () => setScopeHovered(false) : undefined, backgroundColor: scopeHovered ? 'userMessageBackgroundHover' : undefined, children: [_jsx(Text, { color: "remember", bold: true, children: scopeRow.left }), _jsx(Text, { dimColor: true, children: `${' '.repeat(scopeRow.gap)}${scopeRow.right}` })] }), _jsx(SearchBox, { query: activeQuery, isFocused: mode === 'list', isTerminalFocused: isTerminalFocused, placeholder: truncateWidth(searchPlaceholder, inputBudget), borderless: true, width: "100%" })] }), _jsxs("ink-box", { style: { flexDirection: 'row', gap: workspaceGap, flexGrow: 1, flexShrink: 1, overflow: 'hidden' }, children: [workspaceAreaVisible && (_jsxs("ink-box", { style: {
|
|
859
|
-
flexDirection: 'column',
|
|
860
|
-
width: workspaceRail ? workspaceWidth : columns,
|
|
861
|
-
height: listHeight,
|
|
862
|
-
flexShrink: 0,
|
|
863
|
-
overflow: 'hidden',
|
|
864
|
-
}, onWheel: handleWorkspaceWheel, children: [!loaded && (_jsx(Text, { dimColor: true, italic: true, children: ` ${truncateWidth(t('session-loading'), workspaceWidth - 2)}` })), loaded && filteredWorkspaceChoices.length === 0 && (_jsx(Text, { dimColor: true, italic: true, children: ` ${truncateWidth(t('session-workspace-no-match'), workspaceWidth - 2)}` })), visibleWorkspaces.map((choice, index) => (_jsx(WorkspaceListRow, { workspace: choice.kind === 'workspace' ? choice.workspace : undefined, all: choice.kind === 'all', totalProjects: workspaceGroups.length, totalSessions: totalWorkspaceSessions, width: workspaceRail ? workspaceWidth : columns, focused: workspaceTop + index === workspaceFocus && level === 'workspaces', selected: choice.id === selectedWorkspaceId, home: home, now: now, onClick: mode === 'list' ? () => chooseWorkspace(choice) : undefined }, choice.id)))] })), sessionAreaVisible && (_jsxs("ink-box", { style: {
|
|
865
|
-
flexDirection: 'row',
|
|
866
|
-
width: sessionAreaWidth,
|
|
867
|
-
height: listHeight,
|
|
868
|
-
flexShrink: 0,
|
|
869
|
-
overflow: 'hidden',
|
|
870
|
-
}, onWheel: handleSessionWheel, children: [!soloPreview && (_jsxs(Box, { flexDirection: "column", width: listWidth, height: listHeight, flexShrink: 0, children: [!loaded && (_jsx(Text, { dimColor: true, italic: true, children: ` ${truncateWidth(t('session-loading'), listWidth - 2)}` })), loaded && view.rows.length === 0 && (_jsx(Text, { dimColor: true, italic: true, children: ` ${truncateWidth(t('resume-none-in-cwd'), listWidth - 2)}` })), visible.map((row, index) => row.kind === 'project' ? (_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: "planMode", bold: true, children: truncateWidth(` ▣ ${formatProject(row.project, home)}`, listWidth - 8) }), _jsx(Text, { dimColor: true, children: ` ${row.count}` })] }, `project:${row.project}:${index}`)) : row.kind === 'pin' ? (_jsxs(Box, { flexShrink: 0, children: [_jsx(Text, { color: "remember", bold: true, children: truncateWidth(` ★ ${t('session-pinned-group')}`, listWidth - 8) }), _jsx(Text, { dimColor: true, children: ` ${row.count}` })] }, "pin-header")) : (_jsx(SessionListRow, { session: row.session, width: listWidth, depth: row.depth, focused: windowTop + index === focus && level === 'sessions', pinned: pins.has(row.session.id), now: now, onClick: mode === 'list' && menu === undefined ? () => {
|
|
871
|
-
if (actionPendingRef.current)
|
|
872
|
-
return;
|
|
873
|
-
setLevel('sessions');
|
|
874
|
-
setFocusId(row.session.id);
|
|
875
|
-
resumeSession(row.session);
|
|
876
|
-
} : undefined, onTogglePin: mode === 'list' && menu === undefined && !actionPendingRef.current
|
|
877
|
-
? () => {
|
|
878
|
-
setFocusId(row.session.id);
|
|
879
|
-
togglePin(row.session);
|
|
880
|
-
}
|
|
881
|
-
: undefined, onContextMenu: mode === 'list' ? (event) => {
|
|
882
|
-
if (actionPendingRef.current)
|
|
883
|
-
return;
|
|
884
|
-
setLevel('sessions');
|
|
885
|
-
setFocusId(row.session.id);
|
|
886
|
-
// Write the ref alongside the state: the keyboard
|
|
887
|
-
// gate reads menuRef, so a right-click followed by a
|
|
888
|
-
// keystroke in the same stdin chunk must see the
|
|
889
|
-
// menu as open.
|
|
890
|
-
const next = { sessionId: row.session.id, col: event.col, row: event.row, item: 0 };
|
|
891
|
-
menuRef.current = next;
|
|
892
|
-
setMenu(next);
|
|
893
|
-
} : undefined }, row.session.id)))] })), (splitPreview || soloPreview) && focused !== undefined && (_jsx(SessionPreview, { session: focused, entries: preview, loading: previewLoading, width: previewWidth, height: listHeight, home: home, now: now }))] }))] }), _jsx(Box, { flexShrink: 0, children: _jsx(Text, { color: notice?.tone === 'error' ? 'error' : 'success', children: notice === undefined
|
|
894
|
-
? ' '
|
|
895
|
-
: ` ${notice.tone === 'error' ? MULTIPLICATION_X : TICK} ${truncateWidth(notice.text, Math.max(0, columns - 4))}` }) }), mode === 'confirm-delete' && focused !== undefined && (_jsx(Box, { flexShrink: 0, onClick: () => {
|
|
896
|
-
// 点击确认行 = 确认删除(与 Enter 同路径);确认屏本身就是
|
|
897
|
-
// 显式确认层,取消保留键盘 Esc,防误点
|
|
898
|
-
setMode('list');
|
|
899
|
-
runDelete(focused);
|
|
900
|
-
}, onMouseEnter: () => setConfirmHovered(true), onMouseLeave: () => setConfirmHovered(false), backgroundColor: confirmHovered ? 'userMessageBackgroundHover' : undefined, children: _jsx(Text, { color: "error", children: ` ${truncateWidth(t('resume-delete-confirm', { name: focused.title.text }), inputBudget)}` }) })), mode === 'confirm-clean' && (_jsx(Box, { flexShrink: 0, onClick: () => {
|
|
901
|
-
setMode('list');
|
|
902
|
-
runClean();
|
|
903
|
-
}, onMouseEnter: () => setConfirmHovered(true), onMouseLeave: () => setConfirmHovered(false), backgroundColor: confirmHovered ? 'userMessageBackgroundHover' : undefined, children: _jsx(Text, { color: "warning", children: ` ${truncateWidth(t('session-clean-confirm', { n: view.emptyCount }), inputBudget)}` }) })), mode === 'rename' && (_jsx(Box, { flexShrink: 0, children: _jsx(SearchBox, { query: tailWidth(renameText, inputBudget), isFocused: true, isTerminalFocused: isTerminalFocused, placeholder: truncateWidth(t('resume-rename-placeholder'), inputBudget), prefix: "\u270E", borderless: true,
|
|
904
|
-
// 同搜索卡片:自适应宽度会让窗口化预算跟随内容收缩,
|
|
905
|
-
// 追加字符后预填标题的头部被丢弃。
|
|
906
|
-
width: "100%" }) })), rules.has(2) && (_jsx(Box, { flexShrink: 0, children: _jsx(Divider, { bleed: true }) })), _jsx(Box, { flexShrink: 0, children: _jsx(Text, { dimColor: true, italic: true, children: _jsx(HintLine, { text: hint }) }) }), menu !== undefined && menuTarget !== undefined && (_jsx(Box, { position: "absolute", left: Math.max(inset.x, Math.min(menu.col + 1, inset.x + Math.max(0, columns - MENU_WIDTH))), top: Math.max(inset.y, Math.min(menu.row + 1, inset.y + Math.max(0, rows - MENU_HEIGHT))), width: MENU_WIDTH, height: MENU_HEIGHT, flexDirection: "column", flexShrink: 0, borderStyle: "round", borderColor: "permission", backgroundColor: "toolCardBackground", children: MENU_ACTIONS.map((action, index) => (_jsx(Box, { height: 1, flexShrink: 0, backgroundColor: index === menu.item ? 'userMessageBackgroundHover' : undefined, onMouseEnter: () => setMenu(m => (m === undefined ? m : { ...m, item: index })), onClick: () => {
|
|
907
|
-
if (actionPendingRef.current)
|
|
908
|
-
return;
|
|
909
|
-
activateMenu(menuTarget, index);
|
|
910
|
-
}, children: _jsx(Text, { color: action === 'delete' ? 'error' : undefined, children: ` ${index === menu.item ? '❯' : ' '} ${t(menuLabelKey(action, pins.has(menuTarget.id)))}` }) }, action))) }))] }));
|
|
911
|
-
}
|