diff --git a/README.md b/README.md index 13ae4c3..923c7b9 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ dispatch | `S` | Toggle sort direction | | `Tab` | Cycle grouping mode | | `p` | Toggle preview panel | +| `P` | Cycle preview position (right → bottom → left → top) | | `PgUp` / `PgDn` | Scroll preview | | `r` | Refresh session store | | `,` | Open settings panel | @@ -248,6 +249,7 @@ Configuration is stored in the platform-specific config directory: | `default_sort` | string | `"updated"` | Sort field: `updated`, `created`, `turns`, `name`, `folder` | | `default_pivot` | string | `"folder"` | Grouping: `none`, `folder`, `repo`, `branch`, `date` | | `show_preview` | bool | `true` | Show preview pane on startup | +| `preview_position` | string | `"right"` | Position of the preview pane: `right`, `bottom`, `left`, `top` | | `max_sessions` | int | `100` | Maximum sessions to load | | `yoloMode` | bool | `false` | Pass `--allow-all` to Copilot CLI (auto-confirm commands) | | `agent` | string | `""` | Pass `--agent ` to Copilot CLI | @@ -272,6 +274,7 @@ Configuration is stored in the platform-specific config directory: "default_sort": "updated", "default_pivot": "folder", "show_preview": true, + "preview_position": "right", "max_sessions": 100, "yoloMode": false, "agent": "", diff --git a/internal/config/config.go b/internal/config/config.go index 05d18c1..c75b18a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -130,6 +130,12 @@ type Config struct { // format. Users can paste any WT scheme JSON directly here. Schemes []styles.ColorScheme `json:"schemes,omitempty"` + // PreviewPosition controls where the session detail/preview panel + // is displayed relative to the session list. + // Valid values: "right", "bottom", "left", "top". + // Default is "right" when unset. + PreviewPosition string `json:"preview_position,omitempty"` + // ConversationNewestFirst controls the turn display order in the // preview panel's Conversation section. When false (default), turns // are shown oldest-first (ascending by TurnIndex). When true, turns @@ -170,6 +176,18 @@ const ( PaneDirectionUp = "up" ) +// PreviewPosition constants control where the preview panel is displayed. +const ( + // PreviewPositionRight places the preview to the right of the session list. + PreviewPositionRight = "right" + // PreviewPositionBottom places the preview below the session list. + PreviewPositionBottom = "bottom" + // PreviewPositionLeft places the preview to the left of the session list. + PreviewPositionLeft = "left" + // PreviewPositionTop places the preview above the session list. + PreviewPositionTop = "top" +) + // EffectivePaneDirection returns the configured pane direction, defaulting // to "auto" when unset. func (c *Config) EffectivePaneDirection() string { @@ -179,6 +197,17 @@ func (c *Config) EffectivePaneDirection() string { return PaneDirectionAuto } +// EffectivePreviewPosition returns the configured preview panel position, +// defaulting to "right" when unset or invalid. +func (c *Config) EffectivePreviewPosition() string { + switch c.PreviewPosition { + case PreviewPositionRight, PreviewPositionBottom, PreviewPositionLeft, PreviewPositionTop: + return c.PreviewPosition + default: + return PreviewPositionRight + } +} + // defaultAttentionThreshold is used when AttentionThreshold is empty or unparseable. const defaultAttentionThreshold = 15 * time.Minute diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a3c6d64..8e45b01 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -60,6 +60,9 @@ func TestDefaultValues(t *testing.T) { if !cfg.WorkspaceRecovery { t.Error("WorkspaceRecovery should default to true") } + if cfg.PreviewPosition != "" { + t.Errorf("PreviewPosition = %q, want empty", cfg.PreviewPosition) + } } // --------------------------------------------------------------------------- @@ -82,6 +85,7 @@ func TestConfigJSONRoundTrip(t *testing.T) { ExcludedDirs: []string{"/tmp", "/var"}, CustomCommand: "ghcs --resume {sessionId} --custom", HiddenSessions: []string{"sess-1", "sess-2"}, + PreviewPosition: "bottom", } data, err := json.Marshal(original) @@ -146,6 +150,9 @@ func TestConfigJSONRoundTrip(t *testing.T) { t.Errorf("HiddenSessions[%d] = %q, want %q", i, restored.HiddenSessions[i], original.HiddenSessions[i]) } } + if restored.PreviewPosition != original.PreviewPosition { + t.Errorf("PreviewPosition = %q, want %q", restored.PreviewPosition, original.PreviewPosition) + } } func TestWorkspaceRecoveryJSONFalse(t *testing.T) { @@ -1035,3 +1042,72 @@ func TestLoad_SanitizesUnsafeShellOnDisk(t *testing.T) { t.Errorf("DefaultTerminal = %q, want empty after sanitize", cfg.DefaultTerminal) } } + +// --------------------------------------------------------------------------- +// EffectivePreviewPosition tests +// --------------------------------------------------------------------------- + +func TestEffectivePreviewPosition_Default(t *testing.T) { + cfg := Default() + if got := cfg.EffectivePreviewPosition(); got != PreviewPositionRight { + t.Errorf("default config: got %q, want %q", got, PreviewPositionRight) + } +} + +func TestEffectivePreviewPosition_EmptyDefaultsToRight(t *testing.T) { + cfg := &Config{} + if got := cfg.EffectivePreviewPosition(); got != PreviewPositionRight { + t.Errorf("empty PreviewPosition: got %q, want %q", got, PreviewPositionRight) + } +} + +func TestEffectivePreviewPosition_InvalidDefaultsToRight(t *testing.T) { + cfg := &Config{PreviewPosition: "invalid"} + if got := cfg.EffectivePreviewPosition(); got != PreviewPositionRight { + t.Errorf("invalid PreviewPosition: got %q, want %q", got, PreviewPositionRight) + } +} + +func TestEffectivePreviewPosition_ExplicitValues(t *testing.T) { + tests := []struct { + pos string + want string + }{ + {PreviewPositionRight, PreviewPositionRight}, + {PreviewPositionBottom, PreviewPositionBottom}, + {PreviewPositionLeft, PreviewPositionLeft}, + {PreviewPositionTop, PreviewPositionTop}, + } + for _, tt := range tests { + cfg := &Config{PreviewPosition: tt.pos} + if got := cfg.EffectivePreviewPosition(); got != tt.want { + t.Errorf("PreviewPosition=%q: got %q, want %q", tt.pos, got, tt.want) + } + } +} + +func TestEffectivePreviewPosition_JSONRoundTrip(t *testing.T) { + jsonStr := `{"preview_position": "bottom"}` + cfg := Default() + if err := json.Unmarshal([]byte(jsonStr), cfg); err != nil { + t.Fatal(err) + } + if got := cfg.EffectivePreviewPosition(); got != PreviewPositionBottom { + t.Errorf("JSON preview_position: got %q, want %q", got, PreviewPositionBottom) + } +} + +func TestPreviewPositionConstants(t *testing.T) { + if PreviewPositionRight != "right" { + t.Errorf("PreviewPositionRight = %q, want 'right'", PreviewPositionRight) + } + if PreviewPositionBottom != "bottom" { + t.Errorf("PreviewPositionBottom = %q, want 'bottom'", PreviewPositionBottom) + } + if PreviewPositionLeft != "left" { + t.Errorf("PreviewPositionLeft = %q, want 'left'", PreviewPositionLeft) + } + if PreviewPositionTop != "top" { + t.Errorf("PreviewPositionTop = %q, want 'top'", PreviewPositionTop) + } +} diff --git a/internal/tui/components/configpanel.go b/internal/tui/components/configpanel.go index f7f1d89..72af941 100644 --- a/internal/tui/components/configpanel.go +++ b/internal/tui/components/configpanel.go @@ -30,6 +30,7 @@ const ( cfgCustomCommand cfgTheme cfgWorkspaceRecovery + cfgPreviewPosition cfgFieldCount ) @@ -50,6 +51,7 @@ type ConfigPanel struct { customCommand string theme string // active color scheme name ("auto" or a scheme name) workspaceRecovery bool + previewPosition string // "right", "bottom", "left", "top" // Available options for cycling. terminals []string @@ -89,6 +91,7 @@ type ConfigValues struct { CustomCommand string Theme string WorkspaceRecovery bool + PreviewPosition string } // SetValues loads the config panel state from external values. @@ -103,6 +106,7 @@ func (c *ConfigPanel) SetValues(v ConfigValues) { c.customCommand = v.CustomCommand c.theme = v.Theme c.workspaceRecovery = v.WorkspaceRecovery + c.previewPosition = v.PreviewPosition } // Values returns the current state of all editable fields. @@ -118,6 +122,7 @@ func (c *ConfigPanel) Values() ConfigValues { CustomCommand: c.customCommand, Theme: c.theme, WorkspaceRecovery: c.workspaceRecovery, + PreviewPosition: c.previewPosition, } } @@ -206,6 +211,8 @@ func (c *ConfigPanel) HandleEnter() tea.Cmd { c.theme = c.cycleTheme(c.theme) case cfgWorkspaceRecovery: c.workspaceRecovery = !c.workspaceRecovery + case cfgPreviewPosition: + c.previewPosition = cyclePreviewPosition(c.previewPosition) default: // cfgFieldCount is a sentinel; no action needed. } @@ -277,6 +284,7 @@ func (c ConfigPanel) View() string { {"Custom Command", stringDisplay(c.customCommand), false}, {"Theme", themeDisplay(c.theme), false}, {"Crash Recovery", boolDisplay(c.workspaceRecovery), false}, + {"Preview Position", previewPositionDisplay(c.previewPosition), false}, } var body strings.Builder @@ -459,3 +467,33 @@ func (c *ConfigPanel) cycleTheme(current string) string { } return c.themeNames[0] } + +// cyclePreviewPosition cycles through the four preview positions. +func cyclePreviewPosition(current string) string { + switch current { + case config.PreviewPositionRight: + return config.PreviewPositionBottom + case config.PreviewPositionBottom: + return config.PreviewPositionLeft + case config.PreviewPositionLeft: + return config.PreviewPositionTop + case config.PreviewPositionTop: + return config.PreviewPositionRight + default: + return config.PreviewPositionBottom + } +} + +// previewPositionDisplay renders the preview position as a user-friendly label. +func previewPositionDisplay(pos string) string { + switch pos { + case config.PreviewPositionBottom: + return styles.ConfigValueStyle.Render("Bottom") + case config.PreviewPositionLeft: + return styles.ConfigValueStyle.Render("Left") + case config.PreviewPositionTop: + return styles.ConfigValueStyle.Render("Top") + default: + return styles.ConfigValueStyle.Render("Right") + } +} diff --git a/internal/tui/components/configpanel_test.go b/internal/tui/components/configpanel_test.go index ab1d26e..0f48bd8 100644 --- a/internal/tui/components/configpanel_test.go +++ b/internal/tui/components/configpanel_test.go @@ -34,7 +34,7 @@ func TestConfigPanel_SetValues_RoundTrip(t *testing.T) { cp.SetValues(ConfigValues{ YoloMode: true, Agent: "myagent", Model: "gpt-4", LaunchMode: "tab", Terminal: "Windows Terminal", Shell: "pwsh", CustomCommand: "my-cmd {sessionId}", Theme: "Campbell", - WorkspaceRecovery: true, + WorkspaceRecovery: true, PreviewPosition: "bottom", }) v := cp.Values() @@ -65,6 +65,9 @@ func TestConfigPanel_SetValues_RoundTrip(t *testing.T) { if !v.WorkspaceRecovery { t.Error("workspaceRecovery should be true") } + if v.PreviewPosition != "bottom" { + t.Errorf("previewPosition = %q, want %q", v.PreviewPosition, "bottom") + } } // --------------------------------------------------------------------------- @@ -152,6 +155,36 @@ func TestConfigPanel_HandleEnter_WorkspaceRecoveryToggle(t *testing.T) { } } +func TestConfigPanel_HandleEnter_PreviewPositionCycle(t *testing.T) { + cp := NewConfigPanel() + cp.SetValues(ConfigValues{PreviewPosition: "right"}) + cp.cursor = cfgPreviewPosition + + // right → bottom + cp.HandleEnter() + if pos := cp.Values().PreviewPosition; pos != "bottom" { + t.Errorf("after cycling from right, pos = %q, want %q", pos, "bottom") + } + + // bottom → left + cp.HandleEnter() + if pos := cp.Values().PreviewPosition; pos != "left" { + t.Errorf("after cycling from bottom, pos = %q, want %q", pos, "left") + } + + // left → top + cp.HandleEnter() + if pos := cp.Values().PreviewPosition; pos != "top" { + t.Errorf("after cycling from left, pos = %q, want %q", pos, "top") + } + + // top → right (wraps around) + cp.HandleEnter() + if pos := cp.Values().PreviewPosition; pos != "right" { + t.Errorf("after cycling from top, pos = %q, want %q", pos, "right") + } +} + func TestConfigPanel_HandleEnter_LaunchModeCycle(t *testing.T) { cp := NewConfigPanel() cp.SetValues(ConfigValues{LaunchMode: "window"}) @@ -305,7 +338,7 @@ func TestConfigPanel_View_ContainsFields(t *testing.T) { cp := NewConfigPanel() cp.SetSize(80, 40) view := cp.View() - for _, field := range []string{"Yolo Mode", "Agent", "Model", "Launch Mode", "Terminal", "Shell", "Custom Command", "Theme", "Crash Recovery"} { + for _, field := range []string{"Yolo Mode", "Agent", "Model", "Launch Mode", "Terminal", "Shell", "Custom Command", "Theme", "Crash Recovery", "Preview Position"} { if !strings.Contains(view, field) { t.Errorf("View should contain field %q", field) } @@ -411,3 +444,47 @@ func TestThemeDisplay(t *testing.T) { } } } + +// --------------------------------------------------------------------------- +// Preview position helpers +// --------------------------------------------------------------------------- + +func TestCyclePreviewPosition(t *testing.T) { + tests := []struct { + current string + want string + }{ + {"right", "bottom"}, + {"bottom", "left"}, + {"left", "top"}, + {"top", "right"}, + {"", "bottom"}, // default/empty cycles to bottom + {"invalid", "bottom"}, // invalid cycles to bottom + } + for _, tt := range tests { + got := cyclePreviewPosition(tt.current) + if got != tt.want { + t.Errorf("cyclePreviewPosition(%q) = %q, want %q", tt.current, got, tt.want) + } + } +} + +func TestPreviewPositionDisplay(t *testing.T) { + tests := []struct { + pos string + want string + }{ + {"right", "Right"}, + {"bottom", "Bottom"}, + {"left", "Left"}, + {"top", "Top"}, + {"", "Right"}, // default shows Right + {"invalid", "Right"}, // invalid shows Right + } + for _, tt := range tests { + got := previewPositionDisplay(tt.pos) + if !strings.Contains(got, tt.want) { + t.Errorf("previewPositionDisplay(%q) = %q, want to contain %q", tt.pos, got, tt.want) + } + } +} diff --git a/internal/tui/components/preview.go b/internal/tui/components/preview.go index f6c7153..46996d7 100644 --- a/internal/tui/components/preview.go +++ b/internal/tui/components/preview.go @@ -9,7 +9,7 @@ import ( "github.com/jongio/dispatch/internal/tui/styles" ) -// PreviewPanel renders a right-side detail panel for the selected session. +// PreviewPanel renders a detail panel for the selected session. type PreviewPanel struct { detail *data.SessionDetail attentionStatus data.AttentionStatus diff --git a/internal/tui/keys.go b/internal/tui/keys.go index c9da518..b67eabb 100644 --- a/internal/tui/keys.go +++ b/internal/tui/keys.go @@ -43,6 +43,7 @@ type keyMap struct { SelectAll key.Binding DeselectAll key.Binding ConversationSort key.Binding + PreviewPosition key.Binding ResumeInterrupted key.Binding } @@ -58,7 +59,7 @@ func (k keyMap) FullHelp() [][]key.Binding { {k.Space, k.LaunchAll, k.SelectAll, k.DeselectAll}, {k.Search, k.Escape, k.Filter}, {k.Sort, k.SortOrder, k.Pivot, k.PivotOrder}, - {k.Preview, k.PreviewScrollUp, k.PreviewScrollDown, k.ConversationSort, k.Reindex, k.Config}, + {k.Preview, k.PreviewPosition, k.PreviewScrollUp, k.PreviewScrollDown, k.ConversationSort, k.Reindex, k.Config}, {k.Hide, k.ToggleHidden, k.Star, k.FilterFavorites, k.JumpNextAttention, k.FilterAttention, k.ResumeInterrupted}, {k.TimeRange1, k.TimeRange2, k.TimeRange3, k.TimeRange4}, {k.Help, k.Quit}, @@ -104,5 +105,6 @@ var keys = keyMap{ SelectAll: key.NewBinding(key.WithKeys("a"), key.WithHelp("a", "select all")), DeselectAll: key.NewBinding(key.WithKeys("d"), key.WithHelp("d", "deselect all")), ConversationSort: key.NewBinding(key.WithKeys("o"), key.WithHelp("o", "conversation order")), + PreviewPosition: key.NewBinding(key.WithKeys("P"), key.WithHelp("P", "cycle preview position")), ResumeInterrupted: key.NewBinding(key.WithKeys("R"), key.WithHelp("R", "resume interrupted")), } diff --git a/internal/tui/layout.go b/internal/tui/layout.go new file mode 100644 index 0000000..25e97a6 --- /dev/null +++ b/internal/tui/layout.go @@ -0,0 +1,118 @@ +package tui + +import ( + "github.com/jongio/dispatch/internal/config" + "github.com/jongio/dispatch/internal/tui/styles" +) + +// --------------------------------------------------------------------------- +// Layout — computed once per resize, consumed by all rendering functions. +// --------------------------------------------------------------------------- + +// layout holds the pre-computed panel dimensions produced by recalcLayout. +// Rendering and hit-testing read these fields instead of recomputing them, +// guaranteeing a single source of truth for every frame. +type layout struct { + totalWidth int + totalHeight int + headerHeight int + footerHeight int + contentHeight int + listWidth int + previewWidth int + listHeight int // for vertical splits: height allocated to the session list + previewHeight int // for vertical splits: height allocated to the preview panel + previewPosition string +} + +// recalcLayout recomputes all panel dimensions based on the current terminal +// size, preview visibility, and preview position. It stores the result in +// m.layout and propagates sizes to every sub-component. +func (m *Model) recalcLayout() { + contentH := m.height - styles.HeaderLines - styles.FooterLines + if contentH < 1 { + contentH = 1 + } + + pos := m.previewPosition + previewW := 0 + previewH := 0 + listW := m.width + listH := contentH + + isHorizontal := pos == config.PreviewPositionRight || pos == config.PreviewPositionLeft + isVertical := pos == config.PreviewPositionTop || pos == config.PreviewPositionBottom + + if m.showPreview { + if isHorizontal && m.width >= styles.PreviewMinWidth { + previewW = int(float64(m.width) * styles.PreviewWidthRatio) + previewH = contentH + listW = m.width - previewW - gapWidth + listH = contentH + } else if isVertical && m.height >= styles.PreviewMinHeight { + previewH = int(float64(contentH) * styles.PreviewHeightRatio) + previewW = m.width + listW = m.width + listH = contentH - previewH - 1 // 1-line gap + } + } + + m.layout = layout{ + totalWidth: m.width, + totalHeight: m.height, + headerHeight: styles.HeaderLines, + footerHeight: styles.FooterLines, + contentHeight: contentH, + listWidth: listW, + previewWidth: previewW, + listHeight: listH, + previewHeight: previewH, + previewPosition: pos, + } + + m.sessionList.SetSize(listW, listH) + m.preview.SetSize(previewW, previewH) + m.help.SetSize(m.width, m.height) + m.shellPicker.SetSize(m.width, m.height) + m.filterPanel.SetSize(m.width, m.height) + m.configPanel.SetSize(m.width, m.height) + m.attentionPicker.SetSize(m.width, m.height) +} + +// isOverPreview returns true when the mouse coordinates fall within the +// preview panel area, accounting for the current preview position. +func (m *Model) isOverPreview(x, y int) bool { + if m.layout.previewWidth == 0 || m.layout.previewHeight == 0 { + return false + } + contentY := y - styles.HeaderLines + if contentY < 0 { + return false + } + switch m.layout.previewPosition { + case config.PreviewPositionLeft: + return x < m.layout.previewWidth + case config.PreviewPositionTop: + return contentY < m.layout.previewHeight + case config.PreviewPositionBottom: + return contentY >= m.layout.listHeight+1 // +1 for gap line + default: // right + return x >= m.layout.listWidth+gapWidth + } +} + +// cyclePreviewPosition advances the preview position: right → bottom → left → top → right. +func (m *Model) cyclePreviewPosition() { + switch m.previewPosition { + case config.PreviewPositionRight: + m.previewPosition = config.PreviewPositionBottom + case config.PreviewPositionBottom: + m.previewPosition = config.PreviewPositionLeft + case config.PreviewPositionLeft: + m.previewPosition = config.PreviewPositionTop + case config.PreviewPositionTop: + m.previewPosition = config.PreviewPositionRight + default: + m.previewPosition = config.PreviewPositionBottom + } +} diff --git a/internal/tui/model.go b/internal/tui/model.go index 0990400..1b174fa 100644 --- a/internal/tui/model.go +++ b/internal/tui/model.go @@ -107,20 +107,6 @@ const ( pivotDate = "date" ) -// --------------------------------------------------------------------------- -// Layout — computed once per resize, consumed by all rendering functions. -// --------------------------------------------------------------------------- - -type layout struct { - totalWidth int - totalHeight int - headerHeight int - footerHeight int - contentHeight int - listWidth int - previewWidth int -} - // --------------------------------------------------------------------------- // Root model // --------------------------------------------------------------------------- @@ -165,15 +151,16 @@ type Model struct { spinner spinner.Model // UI toggles. - showPreview bool - showHidden bool - hiddenSet map[string]struct{} // session ID → struct{} for fast hidden-session lookup - favoritedSet map[string]struct{} // session ID → struct{} for fast favorited-session lookup - showFavorited bool - reindexing bool - reindexLog []string // log lines streamed from chronicle reindex - reindexVP viewport.Model // scrollable viewport for reindex overlay - reindexCancel *components.ReindexHandle // cancel handle for running reindex + showPreview bool + previewPosition string // "right", "bottom", "left", "top" + showHidden bool + hiddenSet map[string]struct{} // session ID → struct{} for fast hidden-session lookup + favoritedSet map[string]struct{} // session ID → struct{} for fast favorited-session lookup + showFavorited bool + reindexing bool + reindexLog []string // log lines streamed from chronicle reindex + reindexVP viewport.Model // scrollable viewport for reindex overlay + reindexCancel *components.ReindexHandle // cancel handle for running reindex // Click debounce: delays single-click action so double-click can be // detected without the first click firing prematurely. @@ -233,6 +220,7 @@ func NewModel() Model { CustomCommand: cfg.CustomCommand, Theme: cfg.Theme, WorkspaceRecovery: cfg.WorkspaceRecovery, + PreviewPosition: cfg.EffectivePreviewPosition(), }) // Build the list of available theme names for the config panel. @@ -262,11 +250,12 @@ func NewModel() Model { Field: sortFieldFromConfig(cfg.DefaultSort), Order: data.Descending, }, - timeRange: cfg.DefaultTimeRange, - pivot: cfg.DefaultPivot, - showPreview: cfg.ShowPreview, - hiddenSet: hiddenSet, - favoritedSet: favoritedSet, + timeRange: cfg.DefaultTimeRange, + pivot: cfg.DefaultPivot, + showPreview: cfg.ShowPreview, + previewPosition: cfg.EffectivePreviewPosition(), + hiddenSet: hiddenSet, + favoritedSet: favoritedSet, sessionList: components.NewSessionList(), searchBar: components.NewSearchBar(), @@ -1044,6 +1033,19 @@ func (m Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) { } return m, nil + case key.Matches(msg, keys.PreviewPosition): + m.cyclePreviewPosition() + m.recalcLayout() + m.cfg.PreviewPosition = m.previewPosition + if err := config.Save(m.cfg); err != nil { + m.statusErr = "config save: " + err.Error() + } + if m.showPreview { + m.detailVersion++ + return m, m.loadSelectedDetailCmd() + } + return m, nil + case key.Matches(msg, keys.PreviewScrollUp): if m.showPreview { m.preview.PageUp() @@ -1277,6 +1279,9 @@ func (m *Model) saveConfigFromPanel() { m.cfg.CustomCommand = v.CustomCommand m.cfg.Theme = v.Theme m.cfg.WorkspaceRecovery = v.WorkspaceRecovery + m.cfg.PreviewPosition = v.PreviewPosition + m.previewPosition = v.PreviewPosition + m.recalcLayout() if err := config.Save(m.cfg); err != nil { m.statusErr = "config save: " + err.Error() } @@ -1310,7 +1315,7 @@ func (m Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) { } // Determine which pane the mouse is over. - overPreview := m.layout.previewWidth > 0 && msg.X >= m.layout.listWidth + overPreview := m.isOverPreview(msg.X, msg.Y) switch msg.Button { case tea.MouseButtonWheelUp: @@ -1350,8 +1355,16 @@ func (m Model) handleMouse(msg tea.MouseMsg) (tea.Model, tea.Cmd) { return m.handleFooterClick(msg.X) } // Preview pane click — check conversation sort toggle. - if m.layout.previewWidth > 0 && msg.X >= m.layout.listWidth { - previewRow := msg.Y - styles.HeaderLines - 1 // -1 for top border + if m.isOverPreview(msg.X, msg.Y) { + var previewRow int + switch m.layout.previewPosition { + case config.PreviewPositionTop: + previewRow = msg.Y - styles.HeaderLines - 1 // -1 for top border + case config.PreviewPositionBottom: + previewRow = msg.Y - styles.HeaderLines - m.layout.listHeight - 1 - 1 // gap + top border + default: + previewRow = msg.Y - styles.HeaderLines - 1 + } contentRow := previewRow + m.preview.ScrollOffset() if m.preview.HitConversationSort(contentRow) { newVal := m.preview.ToggleConversationSort() @@ -1637,35 +1650,41 @@ func (m Model) renderMainView() string { sep := m.renderSeparator() footer := m.renderFooter() - // Recompute content height based on actual rendered heights. - headerH := lipgloss.Height(header) + lipgloss.Height(badges) + lipgloss.Height(sep) - footerH := lipgloss.Height(footer) - contentH := m.height - headerH - footerH - if contentH < 1 { - contentH = 1 - } - - // Compute preview width. - previewW := 0 - if m.showPreview && m.width >= styles.PreviewMinWidth { - previewW = int(float64(m.width) * styles.PreviewWidthRatio) - } - listW := m.width - previewW - if previewW > 0 { - listW -= gapWidth - } - - m.sessionList.SetSize(listW, contentH) - m.preview.SetSize(previewW, contentH) + // Use pre-computed layout dimensions from recalcLayout() so that + // rendering and hit-testing always agree on panel positions/sizes. + l := m.layout var content string - if previewW > 0 { + hasPreview := l.previewWidth > 0 && l.previewHeight > 0 + + if hasPreview { gap := strings.Repeat(" ", gapWidth) - content = lipgloss.JoinHorizontal(lipgloss.Top, - m.sessionList.View(), - gap, - m.preview.View(), - ) + switch l.previewPosition { + case config.PreviewPositionLeft: + content = lipgloss.JoinHorizontal(lipgloss.Top, + m.preview.View(), + gap, + m.sessionList.View(), + ) + case config.PreviewPositionTop: + content = lipgloss.JoinVertical(lipgloss.Left, + m.preview.View(), + "", + m.sessionList.View(), + ) + case config.PreviewPositionBottom: + content = lipgloss.JoinVertical(lipgloss.Left, + m.sessionList.View(), + "", + m.preview.View(), + ) + default: // right + content = lipgloss.JoinHorizontal(lipgloss.Top, + m.sessionList.View(), + gap, + m.preview.View(), + ) + } } else { content = m.sessionList.View() } @@ -2455,43 +2474,6 @@ func (m Model) selectedSessionCwd() string { return "" } -// --------------------------------------------------------------------------- -// Layout -// --------------------------------------------------------------------------- - -func (m *Model) recalcLayout() { - contentH := m.height - styles.HeaderLines - styles.FooterLines - if contentH < 1 { - contentH = 1 - } - previewW := 0 - if m.showPreview && m.width >= styles.PreviewMinWidth { - previewW = int(float64(m.width) * styles.PreviewWidthRatio) - } - listW := m.width - previewW - if previewW > 0 { - listW -= gapWidth - } - - m.layout = layout{ - totalWidth: m.width, - totalHeight: m.height, - headerHeight: styles.HeaderLines, - footerHeight: styles.FooterLines, - contentHeight: contentH, - listWidth: listW, - previewWidth: previewW, - } - - m.sessionList.SetSize(listW, contentH) - m.preview.SetSize(previewW, contentH) - m.help.SetSize(m.width, m.height) - m.shellPicker.SetSize(m.width, m.height) - m.filterPanel.SetSize(m.width, m.height) - m.configPanel.SetSize(m.width, m.height) - m.attentionPicker.SetSize(m.width, m.height) -} - // --------------------------------------------------------------------------- // Cleanup // --------------------------------------------------------------------------- diff --git a/internal/tui/model_helpers_test.go b/internal/tui/model_helpers_test.go index 26f771e..e1a9364 100644 --- a/internal/tui/model_helpers_test.go +++ b/internal/tui/model_helpers_test.go @@ -854,6 +854,231 @@ func TestRecalcLayout_NoPreviewNarrowWindow(t *testing.T) { } } +// --------------------------------------------------------------------------- +// recalcLayout — preview position variants +// --------------------------------------------------------------------------- + +func TestRecalcLayout_PositionRight(t *testing.T) { + m := testModelWithLayout() + m.width = 120 + m.height = 40 + m.showPreview = true + m.previewPosition = "right" + m.recalcLayout() + + if m.layout.previewWidth == 0 { + t.Error("right position: previewWidth should be > 0") + } + if m.layout.previewHeight != m.layout.contentHeight { + t.Error("right position: previewHeight should equal contentHeight") + } + if m.layout.listWidth+gapWidth+m.layout.previewWidth != m.width { + t.Errorf("right position: listWidth(%d) + gap(%d) + previewWidth(%d) != width(%d)", + m.layout.listWidth, gapWidth, m.layout.previewWidth, m.width) + } + if m.layout.previewPosition != "right" { + t.Errorf("layout.previewPosition = %q, want %q", m.layout.previewPosition, "right") + } +} + +func TestRecalcLayout_PositionLeft(t *testing.T) { + m := testModelWithLayout() + m.width = 120 + m.height = 40 + m.showPreview = true + m.previewPosition = "left" + m.recalcLayout() + + if m.layout.previewWidth == 0 { + t.Error("left position: previewWidth should be > 0") + } + if m.layout.listWidth+gapWidth+m.layout.previewWidth != m.width { + t.Errorf("left position: listWidth(%d) + gap(%d) + previewWidth(%d) != width(%d)", + m.layout.listWidth, gapWidth, m.layout.previewWidth, m.width) + } +} + +func TestRecalcLayout_PositionBottom(t *testing.T) { + m := testModelWithLayout() + m.width = 120 + m.height = 40 + m.showPreview = true + m.previewPosition = "bottom" + m.recalcLayout() + + if m.layout.previewHeight == 0 { + t.Error("bottom position: previewHeight should be > 0") + } + if m.layout.previewWidth != m.width { + t.Errorf("bottom position: previewWidth = %d, want %d (full width)", m.layout.previewWidth, m.width) + } + if m.layout.listWidth != m.width { + t.Errorf("bottom position: listWidth = %d, want %d (full width)", m.layout.listWidth, m.width) + } + if m.layout.listHeight+1+m.layout.previewHeight != m.layout.contentHeight { + t.Errorf("bottom position: listHeight(%d) + gap(1) + previewHeight(%d) != contentHeight(%d)", + m.layout.listHeight, m.layout.previewHeight, m.layout.contentHeight) + } +} + +func TestRecalcLayout_PositionTop(t *testing.T) { + m := testModelWithLayout() + m.width = 120 + m.height = 40 + m.showPreview = true + m.previewPosition = "top" + m.recalcLayout() + + if m.layout.previewHeight == 0 { + t.Error("top position: previewHeight should be > 0") + } + if m.layout.previewWidth != m.width { + t.Errorf("top position: previewWidth = %d, want %d (full width)", m.layout.previewWidth, m.width) + } + if m.layout.listHeight+1+m.layout.previewHeight != m.layout.contentHeight { + t.Errorf("top position: listHeight(%d) + gap(1) + previewHeight(%d) != contentHeight(%d)", + m.layout.listHeight, m.layout.previewHeight, m.layout.contentHeight) + } +} + +func TestRecalcLayout_VerticalShortTerminal(t *testing.T) { + m := testModelWithLayout() + m.width = 120 + m.height = 10 // below PreviewMinHeight + m.showPreview = true + m.previewPosition = "bottom" + m.recalcLayout() + + if m.layout.previewHeight != 0 { + t.Errorf("short terminal: previewHeight should be 0, got %d", m.layout.previewHeight) + } +} + +// --------------------------------------------------------------------------- +// isOverPreview — mouse hit detection for all positions +// --------------------------------------------------------------------------- + +func TestIsOverPreview_Right(t *testing.T) { + m := testModelWithLayout() + m.width = 120 + m.height = 30 + m.showPreview = true + m.previewPosition = "right" + m.recalcLayout() + + // Click in list area (left portion) — should not be over preview. + if m.isOverPreview(0, styles.HeaderLines+1) { + t.Error("x=0 should not be over preview (right position)") + } + + // Click in preview area (right portion past list+gap). + previewX := m.layout.listWidth + gapWidth + 1 + if !m.isOverPreview(previewX, styles.HeaderLines+1) { + t.Errorf("x=%d should be over preview (right position)", previewX) + } +} + +func TestIsOverPreview_Left(t *testing.T) { + m := testModelWithLayout() + m.width = 120 + m.height = 30 + m.showPreview = true + m.previewPosition = "left" + m.recalcLayout() + + // Click in preview area (left portion). + if !m.isOverPreview(1, styles.HeaderLines+1) { + t.Error("x=1 should be over preview (left position)") + } + + // Click past preview width — should not be over preview. + if m.isOverPreview(m.layout.previewWidth+5, styles.HeaderLines+1) { + t.Error("x past previewWidth should not be over preview (left position)") + } +} + +func TestIsOverPreview_Bottom(t *testing.T) { + m := testModelWithLayout() + m.width = 120 + m.height = 30 + m.showPreview = true + m.previewPosition = "bottom" + m.recalcLayout() + + // Click in list area (top portion) — should not be over preview. + if m.isOverPreview(10, styles.HeaderLines+1) { + t.Error("y in list area should not be over preview (bottom position)") + } + + // Click in preview area (bottom portion past list+gap). + previewY := styles.HeaderLines + m.layout.listHeight + 1 + 1 + if !m.isOverPreview(10, previewY) { + t.Errorf("y=%d should be over preview (bottom position)", previewY) + } +} + +func TestIsOverPreview_Top(t *testing.T) { + m := testModelWithLayout() + m.width = 120 + m.height = 30 + m.showPreview = true + m.previewPosition = "top" + m.recalcLayout() + + // Click in preview area (top portion). + if !m.isOverPreview(10, styles.HeaderLines+1) { + t.Error("y=HeaderLines+1 should be over preview (top position)") + } + + // Click in list area (below preview). + listY := styles.HeaderLines + m.layout.previewHeight + 2 + if m.isOverPreview(10, listY) { + t.Errorf("y=%d should not be over preview (top position)", listY) + } +} + +func TestIsOverPreview_NoPreview(t *testing.T) { + m := testModelWithLayout() + m.width = 120 + m.height = 30 + m.showPreview = false + m.recalcLayout() + + if m.isOverPreview(60, styles.HeaderLines+5) { + t.Error("should never be over preview when preview is disabled") + } +} + +func TestIsOverPreview_AboveHeader(t *testing.T) { + m := testModelWithLayout() + m.width = 120 + m.height = 30 + m.showPreview = true + m.previewPosition = "right" + m.recalcLayout() + + // Click in header area — should not be over preview. + if m.isOverPreview(100, 0) { + t.Error("y=0 (header) should not be over preview") + } +} + +// --------------------------------------------------------------------------- +// cyclePreviewPosition on Model +// --------------------------------------------------------------------------- + +func TestModelCyclePreviewPosition(t *testing.T) { + m := testModelWithLayout() + + cycle := []string{"bottom", "left", "top", "right"} + for _, want := range cycle { + m.cyclePreviewPosition() + if m.previewPosition != want { + t.Errorf("cyclePreviewPosition: got %q, want %q", m.previewPosition, want) + } + } +} + // --------------------------------------------------------------------------- // doubleClickTimeout constant // --------------------------------------------------------------------------- diff --git a/internal/tui/model_search_test.go b/internal/tui/model_search_test.go index 6b22483..6a1ed09 100644 --- a/internal/tui/model_search_test.go +++ b/internal/tui/model_search_test.go @@ -15,15 +15,16 @@ import ( func newTestModel() Model { cfg := config.Default() return Model{ - state: stateSessionList, - cfg: cfg, - filter: data.FilterOptions{}, - sort: data.SortOptions{Field: data.SortByUpdated, Order: data.Descending}, - timeRange: "all", - pivot: pivotNone, - searchBar: components.NewSearchBar(), - sessionList: components.NewSessionList(), - hiddenSet: make(map[string]struct{}), + state: stateSessionList, + cfg: cfg, + filter: data.FilterOptions{}, + sort: data.SortOptions{Field: data.SortByUpdated, Order: data.Descending}, + timeRange: "all", + pivot: pivotNone, + previewPosition: cfg.EffectivePreviewPosition(), + searchBar: components.NewSearchBar(), + sessionList: components.NewSessionList(), + hiddenSet: make(map[string]struct{}), } } diff --git a/internal/tui/model_update_test.go b/internal/tui/model_update_test.go index de15d9e..95ce742 100644 --- a/internal/tui/model_update_test.go +++ b/internal/tui/model_update_test.go @@ -1863,6 +1863,40 @@ func TestHandleKey_PreviewOff(t *testing.T) { } } +func TestHandleKey_PreviewPosition(t *testing.T) { + m := newTestModelWithSize(120, 30) + m.showPreview = true + + // Cycle: right → bottom → left → top → right + positions := []string{"bottom", "left", "top", "right"} + for _, want := range positions { + result, _ := m.Update(runeKeyMsg('P')) + m = result.(Model) + if m.previewPosition != want { + t.Errorf("previewPosition = %q, want %q", m.previewPosition, want) + } + if m.cfg.PreviewPosition != want { + t.Errorf("cfg.PreviewPosition = %q, want %q (should be persisted)", m.cfg.PreviewPosition, want) + } + } +} + +func TestHandleKey_PreviewPosition_PreviewOff(t *testing.T) { + m := newTestModelWithSize(120, 30) + m.showPreview = false + + // Position should still cycle even when preview is hidden. + result, cmd := m.Update(runeKeyMsg('P')) + rm := result.(Model) + if rm.previewPosition != "bottom" { + t.Errorf("previewPosition = %q, want %q", rm.previewPosition, "bottom") + } + // When preview is off, no detail load cmd should be returned. + if cmd != nil { + t.Error("cycling position with preview off should return nil cmd") + } +} + func TestHandleKey_Reindex(t *testing.T) { m := newTestModel() m.reindexing = false diff --git a/internal/tui/screenshot.go b/internal/tui/screenshot.go index a82d650..1c64a54 100644 --- a/internal/tui/screenshot.go +++ b/internal/tui/screenshot.go @@ -12,6 +12,7 @@ import ( "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" + "github.com/jongio/dispatch/internal/config" "github.com/jongio/dispatch/internal/data" "github.com/jongio/dispatch/internal/platform" "github.com/jongio/dispatch/internal/tui/components" @@ -216,6 +217,25 @@ func (c *captureCtx) captureFeatures(subDir string) []Screenshot { add("preview-scroll", m) } + // ── Preview positions ───────────────────────────────────────────── + for _, tc := range []struct { + name string + pos string + }{ + {"preview-right", config.PreviewPositionRight}, + {"preview-bottom", config.PreviewPositionBottom}, + {"preview-left", config.PreviewPositionLeft}, + {"preview-top", config.PreviewPositionTop}, + } { + m := newBase() + m.showPreview = true + m.detail = c.detail + m.preview.SetDetail(c.detail) + m.previewPosition = tc.pos + m.recalcLayout() + add(tc.name, m) + } + // ── Hidden sessions ─────────────────────────────────────────────── { m := newBase() diff --git a/internal/tui/styles/theme.go b/internal/tui/styles/theme.go index 84bd125..319ea01 100644 --- a/internal/tui/styles/theme.go +++ b/internal/tui/styles/theme.go @@ -333,9 +333,17 @@ const ( PreviewMinWidth = 80 // PreviewWidthRatio is the fraction of the total width allocated to - // the preview panel when it is visible. + // the preview panel when it is visible (used for left/right positions). PreviewWidthRatio = 0.38 + // PreviewHeightRatio is the fraction of the total height allocated to + // the preview panel when displayed at the top or bottom. + PreviewHeightRatio = 0.40 + + // PreviewMinHeight is the minimum terminal height at which the preview + // panel becomes visible for top/bottom positions. + PreviewMinHeight = 20 + // HeaderLines is the number of lines reserved for the header area // (title + badges + separator). HeaderLines = 3 // header + badges + separator diff --git a/web/public/screenshots/campbell/attention-picker.png b/web/public/screenshots/campbell/attention-picker.png index 586a440..44c9b6c 100644 Binary files a/web/public/screenshots/campbell/attention-picker.png and b/web/public/screenshots/campbell/attention-picker.png differ diff --git a/web/public/screenshots/campbell/config-editing.png b/web/public/screenshots/campbell/config-editing.png index 57975a0..aad5fde 100644 Binary files a/web/public/screenshots/campbell/config-editing.png and b/web/public/screenshots/campbell/config-editing.png differ diff --git a/web/public/screenshots/campbell/config-panel.png b/web/public/screenshots/campbell/config-panel.png index b3ddaab..8ff4c12 100644 Binary files a/web/public/screenshots/campbell/config-panel.png and b/web/public/screenshots/campbell/config-panel.png differ diff --git a/web/public/screenshots/campbell/empty-state.png b/web/public/screenshots/campbell/empty-state.png index 1e23f2c..6efb883 100644 Binary files a/web/public/screenshots/campbell/empty-state.png and b/web/public/screenshots/campbell/empty-state.png differ diff --git a/web/public/screenshots/campbell/favorites.png b/web/public/screenshots/campbell/favorites.png index eb1549d..66c22c6 100644 Binary files a/web/public/screenshots/campbell/favorites.png and b/web/public/screenshots/campbell/favorites.png differ diff --git a/web/public/screenshots/campbell/help-overlay.png b/web/public/screenshots/campbell/help-overlay.png index 6d22047..0fd5e56 100644 Binary files a/web/public/screenshots/campbell/help-overlay.png and b/web/public/screenshots/campbell/help-overlay.png differ diff --git a/web/public/screenshots/campbell/hero-main.png b/web/public/screenshots/campbell/hero-main.png index 0a0bb7e..0a6a7d5 100644 Binary files a/web/public/screenshots/campbell/hero-main.png and b/web/public/screenshots/campbell/hero-main.png differ diff --git a/web/public/screenshots/campbell/hidden-sessions.png b/web/public/screenshots/campbell/hidden-sessions.png index fcfc2ba..87111b2 100644 Binary files a/web/public/screenshots/campbell/hidden-sessions.png and b/web/public/screenshots/campbell/hidden-sessions.png differ diff --git a/web/public/screenshots/campbell/loading-state.png b/web/public/screenshots/campbell/loading-state.png index 5d46cd7..85f6397 100644 Binary files a/web/public/screenshots/campbell/loading-state.png and b/web/public/screenshots/campbell/loading-state.png differ diff --git a/web/public/screenshots/campbell/multi-select.png b/web/public/screenshots/campbell/multi-select.png index e45f170..6f5da4f 100644 Binary files a/web/public/screenshots/campbell/multi-select.png and b/web/public/screenshots/campbell/multi-select.png differ diff --git a/web/public/screenshots/campbell/pivot-branch.png b/web/public/screenshots/campbell/pivot-branch.png index 7a268cb..1f21f42 100644 Binary files a/web/public/screenshots/campbell/pivot-branch.png and b/web/public/screenshots/campbell/pivot-branch.png differ diff --git a/web/public/screenshots/campbell/pivot-date.png b/web/public/screenshots/campbell/pivot-date.png index 183c29d..7cdfb2a 100644 Binary files a/web/public/screenshots/campbell/pivot-date.png and b/web/public/screenshots/campbell/pivot-date.png differ diff --git a/web/public/screenshots/campbell/pivot-flat.png b/web/public/screenshots/campbell/pivot-flat.png index 2a76cc9..f7ed03c 100644 Binary files a/web/public/screenshots/campbell/pivot-flat.png and b/web/public/screenshots/campbell/pivot-flat.png differ diff --git a/web/public/screenshots/campbell/pivot-folder.png b/web/public/screenshots/campbell/pivot-folder.png index 454f817..073fdc8 100644 Binary files a/web/public/screenshots/campbell/pivot-folder.png and b/web/public/screenshots/campbell/pivot-folder.png differ diff --git a/web/public/screenshots/campbell/pivot-repo.png b/web/public/screenshots/campbell/pivot-repo.png index ba3f580..a260f4c 100644 Binary files a/web/public/screenshots/campbell/pivot-repo.png and b/web/public/screenshots/campbell/pivot-repo.png differ diff --git a/web/public/screenshots/campbell/preview-bottom.png b/web/public/screenshots/campbell/preview-bottom.png new file mode 100644 index 0000000..fe96b4c Binary files /dev/null and b/web/public/screenshots/campbell/preview-bottom.png differ diff --git a/web/public/screenshots/campbell/preview-left.png b/web/public/screenshots/campbell/preview-left.png new file mode 100644 index 0000000..d994b81 Binary files /dev/null and b/web/public/screenshots/campbell/preview-left.png differ diff --git a/web/public/screenshots/campbell/preview-panel.png b/web/public/screenshots/campbell/preview-panel.png index 0a0bb7e..0a6a7d5 100644 Binary files a/web/public/screenshots/campbell/preview-panel.png and b/web/public/screenshots/campbell/preview-panel.png differ diff --git a/web/public/screenshots/campbell/preview-right.png b/web/public/screenshots/campbell/preview-right.png new file mode 100644 index 0000000..0a6a7d5 Binary files /dev/null and b/web/public/screenshots/campbell/preview-right.png differ diff --git a/web/public/screenshots/campbell/preview-scroll.png b/web/public/screenshots/campbell/preview-scroll.png index 145405f..09730cf 100644 Binary files a/web/public/screenshots/campbell/preview-scroll.png and b/web/public/screenshots/campbell/preview-scroll.png differ diff --git a/web/public/screenshots/campbell/preview-top.png b/web/public/screenshots/campbell/preview-top.png new file mode 100644 index 0000000..0d85967 Binary files /dev/null and b/web/public/screenshots/campbell/preview-top.png differ diff --git a/web/public/screenshots/campbell/search-active.png b/web/public/screenshots/campbell/search-active.png index 857f38f..d59431d 100644 Binary files a/web/public/screenshots/campbell/search-active.png and b/web/public/screenshots/campbell/search-active.png differ diff --git a/web/public/screenshots/campbell/search-deep.png b/web/public/screenshots/campbell/search-deep.png index 5ffb285..b5630e9 100644 Binary files a/web/public/screenshots/campbell/search-deep.png and b/web/public/screenshots/campbell/search-deep.png differ diff --git a/web/public/screenshots/campbell/sort-folder.png b/web/public/screenshots/campbell/sort-folder.png index 07269e9..bf104e8 100644 Binary files a/web/public/screenshots/campbell/sort-folder.png and b/web/public/screenshots/campbell/sort-folder.png differ diff --git a/web/public/screenshots/campbell/sort-updated.png b/web/public/screenshots/campbell/sort-updated.png index 454f817..073fdc8 100644 Binary files a/web/public/screenshots/campbell/sort-updated.png and b/web/public/screenshots/campbell/sort-updated.png differ diff --git a/web/public/screenshots/campbell/time-range-1h.png b/web/public/screenshots/campbell/time-range-1h.png index e800bdb..cbcbee8 100644 Binary files a/web/public/screenshots/campbell/time-range-1h.png and b/web/public/screenshots/campbell/time-range-1h.png differ diff --git a/web/public/screenshots/campbell/time-range-7d.png b/web/public/screenshots/campbell/time-range-7d.png index 454f817..073fdc8 100644 Binary files a/web/public/screenshots/campbell/time-range-7d.png and b/web/public/screenshots/campbell/time-range-7d.png differ diff --git a/web/public/screenshots/campbell/time-range-all.png b/web/public/screenshots/campbell/time-range-all.png index ae3593a..2a54226 100644 Binary files a/web/public/screenshots/campbell/time-range-all.png and b/web/public/screenshots/campbell/time-range-all.png differ diff --git a/web/public/screenshots/campbell/tree-collapsed.png b/web/public/screenshots/campbell/tree-collapsed.png index f767462..21ffb2c 100644 Binary files a/web/public/screenshots/campbell/tree-collapsed.png and b/web/public/screenshots/campbell/tree-collapsed.png differ diff --git a/web/public/screenshots/campbell/tree-expanded.png b/web/public/screenshots/campbell/tree-expanded.png index 454f817..073fdc8 100644 Binary files a/web/public/screenshots/campbell/tree-expanded.png and b/web/public/screenshots/campbell/tree-expanded.png differ diff --git a/web/public/screenshots/dispatch-dark/attention-picker.png b/web/public/screenshots/dispatch-dark/attention-picker.png index dd74103..71091d2 100644 Binary files a/web/public/screenshots/dispatch-dark/attention-picker.png and b/web/public/screenshots/dispatch-dark/attention-picker.png differ diff --git a/web/public/screenshots/dispatch-dark/config-editing.png b/web/public/screenshots/dispatch-dark/config-editing.png index d434665..c2eba68 100644 Binary files a/web/public/screenshots/dispatch-dark/config-editing.png and b/web/public/screenshots/dispatch-dark/config-editing.png differ diff --git a/web/public/screenshots/dispatch-dark/config-panel.png b/web/public/screenshots/dispatch-dark/config-panel.png index ecd9428..cbbd793 100644 Binary files a/web/public/screenshots/dispatch-dark/config-panel.png and b/web/public/screenshots/dispatch-dark/config-panel.png differ diff --git a/web/public/screenshots/dispatch-dark/empty-state.png b/web/public/screenshots/dispatch-dark/empty-state.png index 6cef894..581fda3 100644 Binary files a/web/public/screenshots/dispatch-dark/empty-state.png and b/web/public/screenshots/dispatch-dark/empty-state.png differ diff --git a/web/public/screenshots/dispatch-dark/favorites.png b/web/public/screenshots/dispatch-dark/favorites.png index 17d56f7..6ef5af4 100644 Binary files a/web/public/screenshots/dispatch-dark/favorites.png and b/web/public/screenshots/dispatch-dark/favorites.png differ diff --git a/web/public/screenshots/dispatch-dark/help-overlay.png b/web/public/screenshots/dispatch-dark/help-overlay.png index cf469c7..787ccf8 100644 Binary files a/web/public/screenshots/dispatch-dark/help-overlay.png and b/web/public/screenshots/dispatch-dark/help-overlay.png differ diff --git a/web/public/screenshots/dispatch-dark/hero-main.png b/web/public/screenshots/dispatch-dark/hero-main.png index 67c9fcb..a679471 100644 Binary files a/web/public/screenshots/dispatch-dark/hero-main.png and b/web/public/screenshots/dispatch-dark/hero-main.png differ diff --git a/web/public/screenshots/dispatch-dark/hidden-sessions.png b/web/public/screenshots/dispatch-dark/hidden-sessions.png index 70f00fa..6b8121d 100644 Binary files a/web/public/screenshots/dispatch-dark/hidden-sessions.png and b/web/public/screenshots/dispatch-dark/hidden-sessions.png differ diff --git a/web/public/screenshots/dispatch-dark/loading-state.png b/web/public/screenshots/dispatch-dark/loading-state.png index 54a3953..7204d9a 100644 Binary files a/web/public/screenshots/dispatch-dark/loading-state.png and b/web/public/screenshots/dispatch-dark/loading-state.png differ diff --git a/web/public/screenshots/dispatch-dark/multi-select.png b/web/public/screenshots/dispatch-dark/multi-select.png index c42996a..30446a1 100644 Binary files a/web/public/screenshots/dispatch-dark/multi-select.png and b/web/public/screenshots/dispatch-dark/multi-select.png differ diff --git a/web/public/screenshots/dispatch-dark/pivot-branch.png b/web/public/screenshots/dispatch-dark/pivot-branch.png index 0db3993..54a61de 100644 Binary files a/web/public/screenshots/dispatch-dark/pivot-branch.png and b/web/public/screenshots/dispatch-dark/pivot-branch.png differ diff --git a/web/public/screenshots/dispatch-dark/pivot-date.png b/web/public/screenshots/dispatch-dark/pivot-date.png index 4c3ceb0..7fc8f72 100644 Binary files a/web/public/screenshots/dispatch-dark/pivot-date.png and b/web/public/screenshots/dispatch-dark/pivot-date.png differ diff --git a/web/public/screenshots/dispatch-dark/pivot-flat.png b/web/public/screenshots/dispatch-dark/pivot-flat.png index 5397d16..15277bc 100644 Binary files a/web/public/screenshots/dispatch-dark/pivot-flat.png and b/web/public/screenshots/dispatch-dark/pivot-flat.png differ diff --git a/web/public/screenshots/dispatch-dark/pivot-folder.png b/web/public/screenshots/dispatch-dark/pivot-folder.png index 040af73..9f364ba 100644 Binary files a/web/public/screenshots/dispatch-dark/pivot-folder.png and b/web/public/screenshots/dispatch-dark/pivot-folder.png differ diff --git a/web/public/screenshots/dispatch-dark/pivot-repo.png b/web/public/screenshots/dispatch-dark/pivot-repo.png index 4fa87f3..4e286ca 100644 Binary files a/web/public/screenshots/dispatch-dark/pivot-repo.png and b/web/public/screenshots/dispatch-dark/pivot-repo.png differ diff --git a/web/public/screenshots/dispatch-dark/preview-bottom.png b/web/public/screenshots/dispatch-dark/preview-bottom.png new file mode 100644 index 0000000..bdf354d Binary files /dev/null and b/web/public/screenshots/dispatch-dark/preview-bottom.png differ diff --git a/web/public/screenshots/dispatch-dark/preview-left.png b/web/public/screenshots/dispatch-dark/preview-left.png new file mode 100644 index 0000000..1ce56a2 Binary files /dev/null and b/web/public/screenshots/dispatch-dark/preview-left.png differ diff --git a/web/public/screenshots/dispatch-dark/preview-panel.png b/web/public/screenshots/dispatch-dark/preview-panel.png index 67c9fcb..a679471 100644 Binary files a/web/public/screenshots/dispatch-dark/preview-panel.png and b/web/public/screenshots/dispatch-dark/preview-panel.png differ diff --git a/web/public/screenshots/dispatch-dark/preview-right.png b/web/public/screenshots/dispatch-dark/preview-right.png new file mode 100644 index 0000000..a679471 Binary files /dev/null and b/web/public/screenshots/dispatch-dark/preview-right.png differ diff --git a/web/public/screenshots/dispatch-dark/preview-scroll.png b/web/public/screenshots/dispatch-dark/preview-scroll.png index d1629c7..d7b5a90 100644 Binary files a/web/public/screenshots/dispatch-dark/preview-scroll.png and b/web/public/screenshots/dispatch-dark/preview-scroll.png differ diff --git a/web/public/screenshots/dispatch-dark/preview-top.png b/web/public/screenshots/dispatch-dark/preview-top.png new file mode 100644 index 0000000..2eb5a39 Binary files /dev/null and b/web/public/screenshots/dispatch-dark/preview-top.png differ diff --git a/web/public/screenshots/dispatch-dark/search-active.png b/web/public/screenshots/dispatch-dark/search-active.png index 83e7437..18025f0 100644 Binary files a/web/public/screenshots/dispatch-dark/search-active.png and b/web/public/screenshots/dispatch-dark/search-active.png differ diff --git a/web/public/screenshots/dispatch-dark/search-deep.png b/web/public/screenshots/dispatch-dark/search-deep.png index 206fa0a..f54bf33 100644 Binary files a/web/public/screenshots/dispatch-dark/search-deep.png and b/web/public/screenshots/dispatch-dark/search-deep.png differ diff --git a/web/public/screenshots/dispatch-dark/sort-folder.png b/web/public/screenshots/dispatch-dark/sort-folder.png index f876749..44792ec 100644 Binary files a/web/public/screenshots/dispatch-dark/sort-folder.png and b/web/public/screenshots/dispatch-dark/sort-folder.png differ diff --git a/web/public/screenshots/dispatch-dark/sort-updated.png b/web/public/screenshots/dispatch-dark/sort-updated.png index 040af73..9f364ba 100644 Binary files a/web/public/screenshots/dispatch-dark/sort-updated.png and b/web/public/screenshots/dispatch-dark/sort-updated.png differ diff --git a/web/public/screenshots/dispatch-dark/time-range-1h.png b/web/public/screenshots/dispatch-dark/time-range-1h.png index da954a9..e845162 100644 Binary files a/web/public/screenshots/dispatch-dark/time-range-1h.png and b/web/public/screenshots/dispatch-dark/time-range-1h.png differ diff --git a/web/public/screenshots/dispatch-dark/time-range-7d.png b/web/public/screenshots/dispatch-dark/time-range-7d.png index 040af73..9f364ba 100644 Binary files a/web/public/screenshots/dispatch-dark/time-range-7d.png and b/web/public/screenshots/dispatch-dark/time-range-7d.png differ diff --git a/web/public/screenshots/dispatch-dark/time-range-all.png b/web/public/screenshots/dispatch-dark/time-range-all.png index 57f74e8..d953489 100644 Binary files a/web/public/screenshots/dispatch-dark/time-range-all.png and b/web/public/screenshots/dispatch-dark/time-range-all.png differ diff --git a/web/public/screenshots/dispatch-dark/tree-collapsed.png b/web/public/screenshots/dispatch-dark/tree-collapsed.png index 0c0eb32..44b4dc7 100644 Binary files a/web/public/screenshots/dispatch-dark/tree-collapsed.png and b/web/public/screenshots/dispatch-dark/tree-collapsed.png differ diff --git a/web/public/screenshots/dispatch-dark/tree-expanded.png b/web/public/screenshots/dispatch-dark/tree-expanded.png index 040af73..9f364ba 100644 Binary files a/web/public/screenshots/dispatch-dark/tree-expanded.png and b/web/public/screenshots/dispatch-dark/tree-expanded.png differ diff --git a/web/public/screenshots/dispatch-light/attention-picker.png b/web/public/screenshots/dispatch-light/attention-picker.png index 73cd8c3..6cab43f 100644 Binary files a/web/public/screenshots/dispatch-light/attention-picker.png and b/web/public/screenshots/dispatch-light/attention-picker.png differ diff --git a/web/public/screenshots/dispatch-light/config-editing.png b/web/public/screenshots/dispatch-light/config-editing.png index 6184af7..c647b26 100644 Binary files a/web/public/screenshots/dispatch-light/config-editing.png and b/web/public/screenshots/dispatch-light/config-editing.png differ diff --git a/web/public/screenshots/dispatch-light/config-panel.png b/web/public/screenshots/dispatch-light/config-panel.png index c2e62ac..d2c7e79 100644 Binary files a/web/public/screenshots/dispatch-light/config-panel.png and b/web/public/screenshots/dispatch-light/config-panel.png differ diff --git a/web/public/screenshots/dispatch-light/empty-state.png b/web/public/screenshots/dispatch-light/empty-state.png index edbfa03..50801e5 100644 Binary files a/web/public/screenshots/dispatch-light/empty-state.png and b/web/public/screenshots/dispatch-light/empty-state.png differ diff --git a/web/public/screenshots/dispatch-light/favorites.png b/web/public/screenshots/dispatch-light/favorites.png index 6a5b7f6..70efccc 100644 Binary files a/web/public/screenshots/dispatch-light/favorites.png and b/web/public/screenshots/dispatch-light/favorites.png differ diff --git a/web/public/screenshots/dispatch-light/help-overlay.png b/web/public/screenshots/dispatch-light/help-overlay.png index c2cae53..940c6d8 100644 Binary files a/web/public/screenshots/dispatch-light/help-overlay.png and b/web/public/screenshots/dispatch-light/help-overlay.png differ diff --git a/web/public/screenshots/dispatch-light/hero-main.png b/web/public/screenshots/dispatch-light/hero-main.png index 40e5f7c..5115dd4 100644 Binary files a/web/public/screenshots/dispatch-light/hero-main.png and b/web/public/screenshots/dispatch-light/hero-main.png differ diff --git a/web/public/screenshots/dispatch-light/hidden-sessions.png b/web/public/screenshots/dispatch-light/hidden-sessions.png index ef33f36..b2b40bf 100644 Binary files a/web/public/screenshots/dispatch-light/hidden-sessions.png and b/web/public/screenshots/dispatch-light/hidden-sessions.png differ diff --git a/web/public/screenshots/dispatch-light/loading-state.png b/web/public/screenshots/dispatch-light/loading-state.png index 9068d58..f7beb2c 100644 Binary files a/web/public/screenshots/dispatch-light/loading-state.png and b/web/public/screenshots/dispatch-light/loading-state.png differ diff --git a/web/public/screenshots/dispatch-light/multi-select.png b/web/public/screenshots/dispatch-light/multi-select.png index 086eddf..7d50cf5 100644 Binary files a/web/public/screenshots/dispatch-light/multi-select.png and b/web/public/screenshots/dispatch-light/multi-select.png differ diff --git a/web/public/screenshots/dispatch-light/pivot-branch.png b/web/public/screenshots/dispatch-light/pivot-branch.png index 18f5584..1f05567 100644 Binary files a/web/public/screenshots/dispatch-light/pivot-branch.png and b/web/public/screenshots/dispatch-light/pivot-branch.png differ diff --git a/web/public/screenshots/dispatch-light/pivot-date.png b/web/public/screenshots/dispatch-light/pivot-date.png index 6aaf9c3..34b46a8 100644 Binary files a/web/public/screenshots/dispatch-light/pivot-date.png and b/web/public/screenshots/dispatch-light/pivot-date.png differ diff --git a/web/public/screenshots/dispatch-light/pivot-flat.png b/web/public/screenshots/dispatch-light/pivot-flat.png index 9bb3832..a38a349 100644 Binary files a/web/public/screenshots/dispatch-light/pivot-flat.png and b/web/public/screenshots/dispatch-light/pivot-flat.png differ diff --git a/web/public/screenshots/dispatch-light/pivot-folder.png b/web/public/screenshots/dispatch-light/pivot-folder.png index bd34165..88ca1cb 100644 Binary files a/web/public/screenshots/dispatch-light/pivot-folder.png and b/web/public/screenshots/dispatch-light/pivot-folder.png differ diff --git a/web/public/screenshots/dispatch-light/pivot-repo.png b/web/public/screenshots/dispatch-light/pivot-repo.png index 9b19796..4475f12 100644 Binary files a/web/public/screenshots/dispatch-light/pivot-repo.png and b/web/public/screenshots/dispatch-light/pivot-repo.png differ diff --git a/web/public/screenshots/dispatch-light/preview-bottom.png b/web/public/screenshots/dispatch-light/preview-bottom.png new file mode 100644 index 0000000..96e4f37 Binary files /dev/null and b/web/public/screenshots/dispatch-light/preview-bottom.png differ diff --git a/web/public/screenshots/dispatch-light/preview-left.png b/web/public/screenshots/dispatch-light/preview-left.png new file mode 100644 index 0000000..567fa7b Binary files /dev/null and b/web/public/screenshots/dispatch-light/preview-left.png differ diff --git a/web/public/screenshots/dispatch-light/preview-panel.png b/web/public/screenshots/dispatch-light/preview-panel.png index 40e5f7c..5115dd4 100644 Binary files a/web/public/screenshots/dispatch-light/preview-panel.png and b/web/public/screenshots/dispatch-light/preview-panel.png differ diff --git a/web/public/screenshots/dispatch-light/preview-right.png b/web/public/screenshots/dispatch-light/preview-right.png new file mode 100644 index 0000000..5115dd4 Binary files /dev/null and b/web/public/screenshots/dispatch-light/preview-right.png differ diff --git a/web/public/screenshots/dispatch-light/preview-scroll.png b/web/public/screenshots/dispatch-light/preview-scroll.png index f70e43c..ba89e76 100644 Binary files a/web/public/screenshots/dispatch-light/preview-scroll.png and b/web/public/screenshots/dispatch-light/preview-scroll.png differ diff --git a/web/public/screenshots/dispatch-light/preview-top.png b/web/public/screenshots/dispatch-light/preview-top.png new file mode 100644 index 0000000..6c55838 Binary files /dev/null and b/web/public/screenshots/dispatch-light/preview-top.png differ diff --git a/web/public/screenshots/dispatch-light/search-active.png b/web/public/screenshots/dispatch-light/search-active.png index 4a4cc0f..f74b40d 100644 Binary files a/web/public/screenshots/dispatch-light/search-active.png and b/web/public/screenshots/dispatch-light/search-active.png differ diff --git a/web/public/screenshots/dispatch-light/search-deep.png b/web/public/screenshots/dispatch-light/search-deep.png index c257d59..82f0a32 100644 Binary files a/web/public/screenshots/dispatch-light/search-deep.png and b/web/public/screenshots/dispatch-light/search-deep.png differ diff --git a/web/public/screenshots/dispatch-light/sort-folder.png b/web/public/screenshots/dispatch-light/sort-folder.png index 623bf6e..25f8db1 100644 Binary files a/web/public/screenshots/dispatch-light/sort-folder.png and b/web/public/screenshots/dispatch-light/sort-folder.png differ diff --git a/web/public/screenshots/dispatch-light/sort-updated.png b/web/public/screenshots/dispatch-light/sort-updated.png index bd34165..88ca1cb 100644 Binary files a/web/public/screenshots/dispatch-light/sort-updated.png and b/web/public/screenshots/dispatch-light/sort-updated.png differ diff --git a/web/public/screenshots/dispatch-light/time-range-1h.png b/web/public/screenshots/dispatch-light/time-range-1h.png index 35b200b..8b64ee0 100644 Binary files a/web/public/screenshots/dispatch-light/time-range-1h.png and b/web/public/screenshots/dispatch-light/time-range-1h.png differ diff --git a/web/public/screenshots/dispatch-light/time-range-7d.png b/web/public/screenshots/dispatch-light/time-range-7d.png index bd34165..88ca1cb 100644 Binary files a/web/public/screenshots/dispatch-light/time-range-7d.png and b/web/public/screenshots/dispatch-light/time-range-7d.png differ diff --git a/web/public/screenshots/dispatch-light/time-range-all.png b/web/public/screenshots/dispatch-light/time-range-all.png index fa69d09..9e9dd94 100644 Binary files a/web/public/screenshots/dispatch-light/time-range-all.png and b/web/public/screenshots/dispatch-light/time-range-all.png differ diff --git a/web/public/screenshots/dispatch-light/tree-collapsed.png b/web/public/screenshots/dispatch-light/tree-collapsed.png index 0dbce7e..dc076a3 100644 Binary files a/web/public/screenshots/dispatch-light/tree-collapsed.png and b/web/public/screenshots/dispatch-light/tree-collapsed.png differ diff --git a/web/public/screenshots/dispatch-light/tree-expanded.png b/web/public/screenshots/dispatch-light/tree-expanded.png index bd34165..88ca1cb 100644 Binary files a/web/public/screenshots/dispatch-light/tree-expanded.png and b/web/public/screenshots/dispatch-light/tree-expanded.png differ diff --git a/web/public/screenshots/one-half-dark/attention-picker.png b/web/public/screenshots/one-half-dark/attention-picker.png index 6520e7c..f9d126e 100644 Binary files a/web/public/screenshots/one-half-dark/attention-picker.png and b/web/public/screenshots/one-half-dark/attention-picker.png differ diff --git a/web/public/screenshots/one-half-dark/config-editing.png b/web/public/screenshots/one-half-dark/config-editing.png index 0777ded..a5ad52f 100644 Binary files a/web/public/screenshots/one-half-dark/config-editing.png and b/web/public/screenshots/one-half-dark/config-editing.png differ diff --git a/web/public/screenshots/one-half-dark/config-panel.png b/web/public/screenshots/one-half-dark/config-panel.png index 326076f..2ed2727 100644 Binary files a/web/public/screenshots/one-half-dark/config-panel.png and b/web/public/screenshots/one-half-dark/config-panel.png differ diff --git a/web/public/screenshots/one-half-dark/empty-state.png b/web/public/screenshots/one-half-dark/empty-state.png index b46fbbd..fb59016 100644 Binary files a/web/public/screenshots/one-half-dark/empty-state.png and b/web/public/screenshots/one-half-dark/empty-state.png differ diff --git a/web/public/screenshots/one-half-dark/favorites.png b/web/public/screenshots/one-half-dark/favorites.png index 57281ed..eb582db 100644 Binary files a/web/public/screenshots/one-half-dark/favorites.png and b/web/public/screenshots/one-half-dark/favorites.png differ diff --git a/web/public/screenshots/one-half-dark/help-overlay.png b/web/public/screenshots/one-half-dark/help-overlay.png index cf4e2f3..e716116 100644 Binary files a/web/public/screenshots/one-half-dark/help-overlay.png and b/web/public/screenshots/one-half-dark/help-overlay.png differ diff --git a/web/public/screenshots/one-half-dark/hero-main.png b/web/public/screenshots/one-half-dark/hero-main.png index ce6e666..89fc59d 100644 Binary files a/web/public/screenshots/one-half-dark/hero-main.png and b/web/public/screenshots/one-half-dark/hero-main.png differ diff --git a/web/public/screenshots/one-half-dark/hidden-sessions.png b/web/public/screenshots/one-half-dark/hidden-sessions.png index eaecdd4..d117d81 100644 Binary files a/web/public/screenshots/one-half-dark/hidden-sessions.png and b/web/public/screenshots/one-half-dark/hidden-sessions.png differ diff --git a/web/public/screenshots/one-half-dark/loading-state.png b/web/public/screenshots/one-half-dark/loading-state.png index 3fb57a7..677d017 100644 Binary files a/web/public/screenshots/one-half-dark/loading-state.png and b/web/public/screenshots/one-half-dark/loading-state.png differ diff --git a/web/public/screenshots/one-half-dark/multi-select.png b/web/public/screenshots/one-half-dark/multi-select.png index 370e0b8..d671be9 100644 Binary files a/web/public/screenshots/one-half-dark/multi-select.png and b/web/public/screenshots/one-half-dark/multi-select.png differ diff --git a/web/public/screenshots/one-half-dark/pivot-branch.png b/web/public/screenshots/one-half-dark/pivot-branch.png index f56139f..122f3dc 100644 Binary files a/web/public/screenshots/one-half-dark/pivot-branch.png and b/web/public/screenshots/one-half-dark/pivot-branch.png differ diff --git a/web/public/screenshots/one-half-dark/pivot-date.png b/web/public/screenshots/one-half-dark/pivot-date.png index 9967bd5..c06de9c 100644 Binary files a/web/public/screenshots/one-half-dark/pivot-date.png and b/web/public/screenshots/one-half-dark/pivot-date.png differ diff --git a/web/public/screenshots/one-half-dark/pivot-flat.png b/web/public/screenshots/one-half-dark/pivot-flat.png index 79d9e5f..033aff5 100644 Binary files a/web/public/screenshots/one-half-dark/pivot-flat.png and b/web/public/screenshots/one-half-dark/pivot-flat.png differ diff --git a/web/public/screenshots/one-half-dark/pivot-folder.png b/web/public/screenshots/one-half-dark/pivot-folder.png index f0e0f95..1c42a0d 100644 Binary files a/web/public/screenshots/one-half-dark/pivot-folder.png and b/web/public/screenshots/one-half-dark/pivot-folder.png differ diff --git a/web/public/screenshots/one-half-dark/pivot-repo.png b/web/public/screenshots/one-half-dark/pivot-repo.png index 66f6d56..31ec4f7 100644 Binary files a/web/public/screenshots/one-half-dark/pivot-repo.png and b/web/public/screenshots/one-half-dark/pivot-repo.png differ diff --git a/web/public/screenshots/one-half-dark/preview-bottom.png b/web/public/screenshots/one-half-dark/preview-bottom.png new file mode 100644 index 0000000..fda57d0 Binary files /dev/null and b/web/public/screenshots/one-half-dark/preview-bottom.png differ diff --git a/web/public/screenshots/one-half-dark/preview-left.png b/web/public/screenshots/one-half-dark/preview-left.png new file mode 100644 index 0000000..f4cae7c Binary files /dev/null and b/web/public/screenshots/one-half-dark/preview-left.png differ diff --git a/web/public/screenshots/one-half-dark/preview-panel.png b/web/public/screenshots/one-half-dark/preview-panel.png index ce6e666..89fc59d 100644 Binary files a/web/public/screenshots/one-half-dark/preview-panel.png and b/web/public/screenshots/one-half-dark/preview-panel.png differ diff --git a/web/public/screenshots/one-half-dark/preview-right.png b/web/public/screenshots/one-half-dark/preview-right.png new file mode 100644 index 0000000..89fc59d Binary files /dev/null and b/web/public/screenshots/one-half-dark/preview-right.png differ diff --git a/web/public/screenshots/one-half-dark/preview-scroll.png b/web/public/screenshots/one-half-dark/preview-scroll.png index 466dd48..aee341d 100644 Binary files a/web/public/screenshots/one-half-dark/preview-scroll.png and b/web/public/screenshots/one-half-dark/preview-scroll.png differ diff --git a/web/public/screenshots/one-half-dark/preview-top.png b/web/public/screenshots/one-half-dark/preview-top.png new file mode 100644 index 0000000..28a0b9e Binary files /dev/null and b/web/public/screenshots/one-half-dark/preview-top.png differ diff --git a/web/public/screenshots/one-half-dark/search-active.png b/web/public/screenshots/one-half-dark/search-active.png index 03d5522..26aebbf 100644 Binary files a/web/public/screenshots/one-half-dark/search-active.png and b/web/public/screenshots/one-half-dark/search-active.png differ diff --git a/web/public/screenshots/one-half-dark/search-deep.png b/web/public/screenshots/one-half-dark/search-deep.png index 66b5ef4..61213de 100644 Binary files a/web/public/screenshots/one-half-dark/search-deep.png and b/web/public/screenshots/one-half-dark/search-deep.png differ diff --git a/web/public/screenshots/one-half-dark/sort-folder.png b/web/public/screenshots/one-half-dark/sort-folder.png index e60a8ac..1ade04a 100644 Binary files a/web/public/screenshots/one-half-dark/sort-folder.png and b/web/public/screenshots/one-half-dark/sort-folder.png differ diff --git a/web/public/screenshots/one-half-dark/sort-updated.png b/web/public/screenshots/one-half-dark/sort-updated.png index f0e0f95..1c42a0d 100644 Binary files a/web/public/screenshots/one-half-dark/sort-updated.png and b/web/public/screenshots/one-half-dark/sort-updated.png differ diff --git a/web/public/screenshots/one-half-dark/time-range-1h.png b/web/public/screenshots/one-half-dark/time-range-1h.png index 367ec96..67ed76f 100644 Binary files a/web/public/screenshots/one-half-dark/time-range-1h.png and b/web/public/screenshots/one-half-dark/time-range-1h.png differ diff --git a/web/public/screenshots/one-half-dark/time-range-7d.png b/web/public/screenshots/one-half-dark/time-range-7d.png index f0e0f95..1c42a0d 100644 Binary files a/web/public/screenshots/one-half-dark/time-range-7d.png and b/web/public/screenshots/one-half-dark/time-range-7d.png differ diff --git a/web/public/screenshots/one-half-dark/time-range-all.png b/web/public/screenshots/one-half-dark/time-range-all.png index 2f2aa41..558047b 100644 Binary files a/web/public/screenshots/one-half-dark/time-range-all.png and b/web/public/screenshots/one-half-dark/time-range-all.png differ diff --git a/web/public/screenshots/one-half-dark/tree-collapsed.png b/web/public/screenshots/one-half-dark/tree-collapsed.png index e4fb2bc..69466b6 100644 Binary files a/web/public/screenshots/one-half-dark/tree-collapsed.png and b/web/public/screenshots/one-half-dark/tree-collapsed.png differ diff --git a/web/public/screenshots/one-half-dark/tree-expanded.png b/web/public/screenshots/one-half-dark/tree-expanded.png index f0e0f95..1c42a0d 100644 Binary files a/web/public/screenshots/one-half-dark/tree-expanded.png and b/web/public/screenshots/one-half-dark/tree-expanded.png differ diff --git a/web/public/screenshots/one-half-light/attention-picker.png b/web/public/screenshots/one-half-light/attention-picker.png index 687ae0c..4ddbd1e 100644 Binary files a/web/public/screenshots/one-half-light/attention-picker.png and b/web/public/screenshots/one-half-light/attention-picker.png differ diff --git a/web/public/screenshots/one-half-light/config-editing.png b/web/public/screenshots/one-half-light/config-editing.png index 337e2d6..d7ddb0f 100644 Binary files a/web/public/screenshots/one-half-light/config-editing.png and b/web/public/screenshots/one-half-light/config-editing.png differ diff --git a/web/public/screenshots/one-half-light/config-panel.png b/web/public/screenshots/one-half-light/config-panel.png index 7d1e620..fd1eec5 100644 Binary files a/web/public/screenshots/one-half-light/config-panel.png and b/web/public/screenshots/one-half-light/config-panel.png differ diff --git a/web/public/screenshots/one-half-light/empty-state.png b/web/public/screenshots/one-half-light/empty-state.png index 396f2e2..2f89e51 100644 Binary files a/web/public/screenshots/one-half-light/empty-state.png and b/web/public/screenshots/one-half-light/empty-state.png differ diff --git a/web/public/screenshots/one-half-light/favorites.png b/web/public/screenshots/one-half-light/favorites.png index 2e59d09..6de366a 100644 Binary files a/web/public/screenshots/one-half-light/favorites.png and b/web/public/screenshots/one-half-light/favorites.png differ diff --git a/web/public/screenshots/one-half-light/help-overlay.png b/web/public/screenshots/one-half-light/help-overlay.png index d62946c..5276b27 100644 Binary files a/web/public/screenshots/one-half-light/help-overlay.png and b/web/public/screenshots/one-half-light/help-overlay.png differ diff --git a/web/public/screenshots/one-half-light/hero-main.png b/web/public/screenshots/one-half-light/hero-main.png index f6eeec3..cf9a029 100644 Binary files a/web/public/screenshots/one-half-light/hero-main.png and b/web/public/screenshots/one-half-light/hero-main.png differ diff --git a/web/public/screenshots/one-half-light/hidden-sessions.png b/web/public/screenshots/one-half-light/hidden-sessions.png index 3069d09..68bf313 100644 Binary files a/web/public/screenshots/one-half-light/hidden-sessions.png and b/web/public/screenshots/one-half-light/hidden-sessions.png differ diff --git a/web/public/screenshots/one-half-light/loading-state.png b/web/public/screenshots/one-half-light/loading-state.png index f2ac908..d12a6b1 100644 Binary files a/web/public/screenshots/one-half-light/loading-state.png and b/web/public/screenshots/one-half-light/loading-state.png differ diff --git a/web/public/screenshots/one-half-light/multi-select.png b/web/public/screenshots/one-half-light/multi-select.png index 604e2e9..f025ee2 100644 Binary files a/web/public/screenshots/one-half-light/multi-select.png and b/web/public/screenshots/one-half-light/multi-select.png differ diff --git a/web/public/screenshots/one-half-light/pivot-branch.png b/web/public/screenshots/one-half-light/pivot-branch.png index 58c7a4b..5dc54ce 100644 Binary files a/web/public/screenshots/one-half-light/pivot-branch.png and b/web/public/screenshots/one-half-light/pivot-branch.png differ diff --git a/web/public/screenshots/one-half-light/pivot-date.png b/web/public/screenshots/one-half-light/pivot-date.png index 66b355c..97b31ce 100644 Binary files a/web/public/screenshots/one-half-light/pivot-date.png and b/web/public/screenshots/one-half-light/pivot-date.png differ diff --git a/web/public/screenshots/one-half-light/pivot-flat.png b/web/public/screenshots/one-half-light/pivot-flat.png index c113d5b..ad20193 100644 Binary files a/web/public/screenshots/one-half-light/pivot-flat.png and b/web/public/screenshots/one-half-light/pivot-flat.png differ diff --git a/web/public/screenshots/one-half-light/pivot-folder.png b/web/public/screenshots/one-half-light/pivot-folder.png index 969e838..ec4753d 100644 Binary files a/web/public/screenshots/one-half-light/pivot-folder.png and b/web/public/screenshots/one-half-light/pivot-folder.png differ diff --git a/web/public/screenshots/one-half-light/pivot-repo.png b/web/public/screenshots/one-half-light/pivot-repo.png index 2c8d354..a3e166b 100644 Binary files a/web/public/screenshots/one-half-light/pivot-repo.png and b/web/public/screenshots/one-half-light/pivot-repo.png differ diff --git a/web/public/screenshots/one-half-light/preview-bottom.png b/web/public/screenshots/one-half-light/preview-bottom.png new file mode 100644 index 0000000..ae62df8 Binary files /dev/null and b/web/public/screenshots/one-half-light/preview-bottom.png differ diff --git a/web/public/screenshots/one-half-light/preview-left.png b/web/public/screenshots/one-half-light/preview-left.png new file mode 100644 index 0000000..282e6e5 Binary files /dev/null and b/web/public/screenshots/one-half-light/preview-left.png differ diff --git a/web/public/screenshots/one-half-light/preview-panel.png b/web/public/screenshots/one-half-light/preview-panel.png index f6eeec3..cf9a029 100644 Binary files a/web/public/screenshots/one-half-light/preview-panel.png and b/web/public/screenshots/one-half-light/preview-panel.png differ diff --git a/web/public/screenshots/one-half-light/preview-right.png b/web/public/screenshots/one-half-light/preview-right.png new file mode 100644 index 0000000..cf9a029 Binary files /dev/null and b/web/public/screenshots/one-half-light/preview-right.png differ diff --git a/web/public/screenshots/one-half-light/preview-scroll.png b/web/public/screenshots/one-half-light/preview-scroll.png index fcf252a..dba821f 100644 Binary files a/web/public/screenshots/one-half-light/preview-scroll.png and b/web/public/screenshots/one-half-light/preview-scroll.png differ diff --git a/web/public/screenshots/one-half-light/preview-top.png b/web/public/screenshots/one-half-light/preview-top.png new file mode 100644 index 0000000..e0efaa5 Binary files /dev/null and b/web/public/screenshots/one-half-light/preview-top.png differ diff --git a/web/public/screenshots/one-half-light/search-active.png b/web/public/screenshots/one-half-light/search-active.png index c0a16c1..255dbef 100644 Binary files a/web/public/screenshots/one-half-light/search-active.png and b/web/public/screenshots/one-half-light/search-active.png differ diff --git a/web/public/screenshots/one-half-light/search-deep.png b/web/public/screenshots/one-half-light/search-deep.png index 357786c..7c71fef 100644 Binary files a/web/public/screenshots/one-half-light/search-deep.png and b/web/public/screenshots/one-half-light/search-deep.png differ diff --git a/web/public/screenshots/one-half-light/sort-folder.png b/web/public/screenshots/one-half-light/sort-folder.png index 568c096..9be242c 100644 Binary files a/web/public/screenshots/one-half-light/sort-folder.png and b/web/public/screenshots/one-half-light/sort-folder.png differ diff --git a/web/public/screenshots/one-half-light/sort-updated.png b/web/public/screenshots/one-half-light/sort-updated.png index 969e838..ec4753d 100644 Binary files a/web/public/screenshots/one-half-light/sort-updated.png and b/web/public/screenshots/one-half-light/sort-updated.png differ diff --git a/web/public/screenshots/one-half-light/time-range-1h.png b/web/public/screenshots/one-half-light/time-range-1h.png index a699103..cc92d2b 100644 Binary files a/web/public/screenshots/one-half-light/time-range-1h.png and b/web/public/screenshots/one-half-light/time-range-1h.png differ diff --git a/web/public/screenshots/one-half-light/time-range-7d.png b/web/public/screenshots/one-half-light/time-range-7d.png index 969e838..ec4753d 100644 Binary files a/web/public/screenshots/one-half-light/time-range-7d.png and b/web/public/screenshots/one-half-light/time-range-7d.png differ diff --git a/web/public/screenshots/one-half-light/time-range-all.png b/web/public/screenshots/one-half-light/time-range-all.png index 21d081d..1ccde69 100644 Binary files a/web/public/screenshots/one-half-light/time-range-all.png and b/web/public/screenshots/one-half-light/time-range-all.png differ diff --git a/web/public/screenshots/one-half-light/tree-collapsed.png b/web/public/screenshots/one-half-light/tree-collapsed.png index 51a1bda..13bd238 100644 Binary files a/web/public/screenshots/one-half-light/tree-collapsed.png and b/web/public/screenshots/one-half-light/tree-collapsed.png differ diff --git a/web/public/screenshots/one-half-light/tree-expanded.png b/web/public/screenshots/one-half-light/tree-expanded.png index 969e838..ec4753d 100644 Binary files a/web/public/screenshots/one-half-light/tree-expanded.png and b/web/public/screenshots/one-half-light/tree-expanded.png differ diff --git a/web/src/components/Carousel.astro b/web/src/components/Carousel.astro new file mode 100644 index 0000000..13d98b7 --- /dev/null +++ b/web/src/components/Carousel.astro @@ -0,0 +1,232 @@ +--- +// Carousel.astro — displays one slotted child at a time with prev/next navigation. +// Each direct child with class "screenshot" is treated as a slide. +// Lightbox overlays (.lightbox) are relocated to so the track's CSS +// transform does not create a containing block that breaks position:fixed. +--- + + + + + + diff --git a/web/src/pages/config.astro b/web/src/pages/config.astro index 42820fe..2236042 100644 --- a/web/src/pages/config.astro +++ b/web/src/pages/config.astro @@ -32,6 +32,7 @@ import ConfigTable from '../components/ConfigTable.astro'; { key: 'default_sort', type: 'string', default: '"updated"', description: 'Sort field: updated, created, turns, name, folder.' }, { key: 'default_pivot', type: 'string', default: '"folder"', description: 'Grouping mode: none, folder, repo, branch, date.' }, { key: 'show_preview', type: 'bool', default: 'true', description: 'Show preview pane on startup.' }, + { key: 'preview_position', type: 'string', default: '"right"', description: 'Position of the preview pane: right, bottom, left, top.' }, { key: 'max_sessions', type: 'int', default: '100', description: 'Maximum sessions to load from the store.' }, { key: 'yoloMode', type: 'bool', default: 'false', description: 'Pass --allow-all to Copilot CLI (auto-confirm commands).' }, { key: 'agent', type: 'string', default: '""', description: 'Pass --agent to Copilot CLI.' }, @@ -57,6 +58,7 @@ import ConfigTable from '../components/ConfigTable.astro'; "default_sort": "updated", "default_pivot": "folder", "show_preview": true, + "preview_position": "right", "max_sessions": 100, "yoloMode": false, "agent": "", diff --git a/web/src/pages/features.astro b/web/src/pages/features.astro index 732d57f..1e6a469 100644 --- a/web/src/pages/features.astro +++ b/web/src/pages/features.astro @@ -1,6 +1,7 @@ --- import Base from '../layouts/Base.astro'; import Screenshot from '../components/Screenshot.astro'; +import Carousel from '../components/Carousel.astro'; --- @@ -160,6 +161,34 @@ import Screenshot from '../components/Screenshot.astro'; + +
+
+
+

Preview Position

+

+ Cycle the preview pane through four positions with the P + key: right (default), bottom, left, and top. The chosen position is + saved to configuration and restored on next launch. +

+
    +
  • Right — preview beside the session list (default)
  • +
  • Bottom — preview below the session list
  • +
  • Left — preview to the left of the session list
  • +
  • Top — preview above the session list
  • +
+
+
+ + + + + + +
+
+
+
diff --git a/web/src/pages/keys.astro b/web/src/pages/keys.astro index c69b797..8f2c5e0 100644 --- a/web/src/pages/keys.astro +++ b/web/src/pages/keys.astro @@ -64,6 +64,7 @@ import KeyTable from '../components/KeyTable.astro'; { keys: 'S', action: 'Toggle sort direction' }, { keys: 'Tab', action: 'Cycle grouping mode' }, { keys: 'p', action: 'Toggle preview panel' }, + { keys: 'P', action: 'Cycle preview position (right → bottom → left → top)' }, { keys: 'PgUp', action: 'Scroll preview up' }, { keys: 'PgDn', action: 'Scroll preview down' }, { keys: 'r', action: 'Reindex sessions' },