v0.25.0 2e6c485

session_state.go RawHistoryFolder

1package entity
2
3import "time"
4
5// SessionStateVersion is the current schema version for session state.
6// Increment when making breaking changes to the serialization format.
7const SessionStateVersion = 1
8
9// SessionState represents a complete snapshot of a browser session.
10// This is serialized to JSON and stored in the database.
11type SessionState struct {
12	Version        int           `json:"version"`
13	SessionID      SessionID     `json:"session_id"`
14	Tabs           []TabSnapshot `json:"tabs"`
15	ActiveTabIndex int           `json:"active_tab_index"`
16	SavedAt        time.Time     `json:"saved_at"`
17}
18
19// TabSnapshot captures the state of a single tab.
20type TabSnapshot struct {
21	ID        TabID             `json:"id"`
22	Name      string            `json:"name"`
23	Position  int               `json:"position"`
24	IsPinned  bool              `json:"is_pinned"`
25	Workspace WorkspaceSnapshot `json:"workspace"`
26}
27
28// WorkspaceSnapshot captures the pane tree layout.
29type WorkspaceSnapshot struct {
30	ID           WorkspaceID       `json:"id"`
31	Root         *PaneNodeSnapshot `json:"root"`
32	ActivePaneID PaneID            `json:"active_pane_id"`
33}
34
35// PaneNodeSnapshot captures a node in the pane tree.
36type PaneNodeSnapshot struct {
37	ID               string              `json:"id"`
38	Pane             *PaneSnapshot       `json:"pane,omitempty"`     // Non-nil for leaf nodes
39	Children         []*PaneNodeSnapshot `json:"children,omitempty"` // Non-nil for containers
40	SplitDir         SplitDirection      `json:"split_dir"`
41	SplitRatio       float64             `json:"split_ratio"`
42	IsStacked        bool                `json:"is_stacked"`
43	ActiveStackIndex int                 `json:"active_stack_index"`
44}
45
46// PaneSnapshot captures the essential state of a pane.
47type PaneSnapshot struct {
48	ID         PaneID  `json:"id"`
49	URI        string  `json:"uri"`
50	Title      string  `json:"title"`
51	ZoomFactor float64 `json:"zoom_factor"`
52}
53
54// SnapshotFromTabList creates a SessionState from a live TabList.
55func SnapshotFromTabList(sessionID SessionID, tabs *TabList) *SessionState {
56	if tabs == nil {
57		return &SessionState{
58			Version:   SessionStateVersion,
59			SessionID: sessionID,
60			Tabs:      []TabSnapshot{},
61			SavedAt:   time.Now(),
62		}
63	}
64
65	snapTabs := make([]TabSnapshot, 0, len(tabs.Tabs))
66	activeTabIndex := 0
67
68	for i, tab := range tabs.Tabs {
69		if tab.ID == tabs.ActiveTabID {
70			activeTabIndex = i
71		}
72		snapTabs = append(snapTabs, snapshotTab(tab))
73	}
74
75	return &SessionState{
76		Version:        SessionStateVersion,
77		SessionID:      sessionID,
78		Tabs:           snapTabs,
79		ActiveTabIndex: activeTabIndex,
80		SavedAt:        time.Now(),
81	}
82}
83
84func snapshotTab(tab *Tab) TabSnapshot {
85	return TabSnapshot{
86		ID:        tab.ID,
87		Name:      tab.Name,
88		Position:  tab.Position,
89		IsPinned:  tab.IsPinned,
90		Workspace: snapshotWorkspace(tab.Workspace),
91	}
92}
93
94func snapshotWorkspace(ws *Workspace) WorkspaceSnapshot {
95	if ws == nil {
96		return WorkspaceSnapshot{}
97	}
98	return WorkspaceSnapshot{
99		ID:           ws.ID,
100		Root:         snapshotPaneNode(ws.Root),
101		ActivePaneID: ws.ActivePaneID,
102	}
103}
104
105func snapshotPaneNode(node *PaneNode) *PaneNodeSnapshot {
106	if node == nil {
107		return nil
108	}
109
110	snapshot := &PaneNodeSnapshot{
111		ID:               node.ID,
112		SplitDir:         node.SplitDir,
113		SplitRatio:       node.SplitRatio,
114		IsStacked:        node.IsStacked,
115		ActiveStackIndex: node.ActiveStackIndex,
116	}
117
118	if node.Pane != nil {
119		snapshot.Pane = &PaneSnapshot{
120			ID:         node.Pane.ID,
121			URI:        node.Pane.URI,
122			Title:      node.Pane.Title,
123			ZoomFactor: node.Pane.ZoomFactor,
124		}
125	}
126
127	if len(node.Children) > 0 {
128		snapshot.Children = make([]*PaneNodeSnapshot, 0, len(node.Children))
129		for _, child := range node.Children {
130			snapshot.Children = append(snapshot.Children, snapshotPaneNode(child))
131		}
132	}
133
134	return snapshot
135}
136
137// SessionInfo provides summary information for the session manager UI.
138type SessionInfo struct {
139	Session   *Session
140	State     *SessionState
141	TabCount  int
142	PaneCount int
143	IsActive  bool      // Has active lock file
144	IsCurrent bool      // Is the current session
145	UpdatedAt time.Time // When the state was last saved
146}
147
148// CountPanes returns the total number of panes in the session state.
149func (s *SessionState) CountPanes() int {
150	count := 0
151	for _, tab := range s.Tabs {
152		count += countPanesInNode(tab.Workspace.Root)
153	}
154	return count
155}
156
157func countPanesInNode(node *PaneNodeSnapshot) int {
158	if node == nil {
159		return 0
160	}
161	if node.Pane != nil {
162		return 1
163	}
164	count := 0
165	for _, child := range node.Children {
166		count += countPanesInNode(child)
167	}
168	return count
169}
170
171// IDGenerator is a function that generates unique IDs.
172type IDGenerator func() string
173
174// TabListFromSnapshot reconstructs a TabList from a SessionState snapshot.
175// Generates new IDs for all entities using the provided generator.
176// This is the inverse of SnapshotFromTabList.
177func TabListFromSnapshot(state *SessionState, idGen IDGenerator) *TabList {
178	if state == nil {
179		return NewTabList()
180	}
181
182	tabs := NewTabList()
183
184	for i, tabSnap := range state.Tabs {
185		tab := tabFromSnapshot(&tabSnap, idGen)
186		if tab == nil {
187			continue
188		}
189		tabs.Tabs = append(tabs.Tabs, tab)
190		tab.Position = len(tabs.Tabs) - 1
191
192		if i == state.ActiveTabIndex {
193			tabs.ActiveTabID = tab.ID
194		}
195	}
196
197	// Ensure we have an active tab
198	if tabs.ActiveTabID == "" && len(tabs.Tabs) > 0 {
199		tabs.ActiveTabID = tabs.Tabs[0].ID
200	}
201
202	return tabs
203}
204
205func tabFromSnapshot(snap *TabSnapshot, idGen IDGenerator) *Tab {
206	if snap == nil {
207		return nil
208	}
209
210	ws := workspaceFromSnapshot(&snap.Workspace, idGen)
211	if ws == nil {
212		return nil
213	}
214
215	return &Tab{
216		ID:        TabID(idGen()),
217		Name:      snap.Name,
218		Workspace: ws,
219		Position:  snap.Position,
220		IsPinned:  snap.IsPinned,
221		CreatedAt: time.Now(),
222	}
223}
224
225func workspaceFromSnapshot(snap *WorkspaceSnapshot, idGen IDGenerator) *Workspace {
226	if snap == nil {
227		return nil
228	}
229
230	root := paneNodeFromSnapshot(snap.Root, nil, idGen)
231	if root == nil {
232		return nil
233	}
234
235	// Find the active pane ID in the restored tree
236	// We need to map the snapshot's ActivePaneID to the new pane ID
237	var activePaneID PaneID
238	root.Walk(func(node *PaneNode) bool {
239		if node.Pane != nil {
240			// Use the first pane as active if we can't find the original
241			if activePaneID == "" {
242				activePaneID = node.Pane.ID
243			}
244		}
245		return true
246	})
247
248	return &Workspace{
249		ID:           WorkspaceID(idGen()),
250		Root:         root,
251		ActivePaneID: activePaneID,
252		CreatedAt:    time.Now(),
253	}
254}
255
256func paneNodeFromSnapshot(snap *PaneNodeSnapshot, parent *PaneNode, idGen IDGenerator) *PaneNode {
257	if snap == nil {
258		return nil
259	}
260
261	node := &PaneNode{
262		ID:               idGen(),
263		Parent:           parent,
264		SplitDir:         snap.SplitDir,
265		SplitRatio:       snap.SplitRatio,
266		IsStacked:        snap.IsStacked,
267		ActiveStackIndex: snap.ActiveStackIndex,
268	}
269
270	// Restore pane if this is a leaf node
271	if snap.Pane != nil {
272		node.Pane = paneFromSnapshot(snap.Pane, idGen)
273	}
274
275	// Restore children recursively
276	if len(snap.Children) > 0 {
277		node.Children = make([]*PaneNode, 0, len(snap.Children))
278		for _, childSnap := range snap.Children {
279			child := paneNodeFromSnapshot(childSnap, node, idGen)
280			if child != nil {
281				node.Children = append(node.Children, child)
282			}
283		}
284	}
285
286	return node
287}
288
289func paneFromSnapshot(snap *PaneSnapshot, idGen IDGenerator) *Pane {
290	if snap == nil {
291		return nil
292	}
293
294	return &Pane{
295		ID:         PaneID(idGen()),
296		URI:        snap.URI,
297		Title:      snap.Title,
298		ZoomFactor: snap.ZoomFactor,
299		WindowType: WindowMain,
300		CreatedAt:  time.Now(),
301	}
302}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 8

Tags 42

↑↓ moveenter openesc close