process_restore.go RawHistoryFolder

1package daemon
2
3import (
4	"path/filepath"
5	"strings"
6	"unicode"
7
8	snapcodec "github.com/bnema/vev/internal/snapshotcodec"
9)
10
11const (
12	processStrategyGeneric  = "generic"
13	processStrategyClaude   = "claude"
14	processStrategyCodex    = "codex"
15	processStrategyOpenCode = "opencode"
16	processStrategyPi       = "pi"
17)
18
19type processRestoreDecision struct {
20	Command string
21	Restore bool
22	Reason  string
23}
24
25func detectProcessStrategy(argv []string) string {
26	if len(argv) == 0 {
27		return processStrategyGeneric
28	}
29	switch filepath.Base(argv[0]) {
30	case "claude":
31		return processStrategyClaude
32	case "codex":
33		return processStrategyCodex
34	case "opencode":
35		return processStrategyOpenCode
36	case "pi":
37		return processStrategyPi
38	default:
39		return processStrategyGeneric
40	}
41}
42
43func extractAgentSessionID(strategy string, argv []string) string {
44	for i, arg := range argv {
45		if !agentSessionFlag(strategy, arg) || i+1 >= len(argv) {
46			continue
47		}
48		candidate := argv[i+1]
49		if candidate != "" && !strings.HasPrefix(candidate, "-") && !containsControl(candidate) {
50			return candidate
51		}
52	}
53	return ""
54}
55
56func agentSessionFlag(strategy, arg string) bool {
57	switch strategy {
58	case processStrategyClaude:
59		return arg == "--session-id" || arg == "--resume" || arg == "-r"
60	case processStrategyOpenCode:
61		return arg == "-s" || arg == "--session"
62	case processStrategyCodex:
63		return arg == "resume"
64	case processStrategyPi:
65		return arg == "--resume" || arg == "-r"
66	default:
67		return false
68	}
69}
70
71func planProcessRestore(proc *snapcodec.Process, allow map[string]struct{}) processRestoreDecision {
72	if proc == nil || len(proc.Argv) == 0 || proc.Argv[0] == "" {
73		return processRestoreDecision{Reason: "missing_process"}
74	}
75	strategy := normalizeProcessStrategy(proc.Strategy, proc.Argv)
76	if _, ok := allow[processAllowlistKey(strategy, proc.Argv)]; !ok {
77		return processRestoreDecision{Reason: "not_allowlisted"}
78	}
79
80	id := proc.Opts.AgentSessionID
81	if containsControl(id) {
82		return processRestoreDecision{Reason: "invalid_agent_session_id"}
83	}
84	switch strategy {
85	case processStrategyPi:
86		if id != "" {
87			return processRestoreDecision{Command: shellQuoteArgvMust([]string{"pi", "--resume", id}), Restore: true}
88		}
89		return processRestoreDecision{Command: "pi --continue", Restore: true}
90	case processStrategyClaude:
91		if id != "" {
92			return processRestoreDecision{Command: shellQuoteArgvMust([]string{"claude", "--resume", id}), Restore: true}
93		}
94		return processRestoreDecision{Command: "claude --continue", Restore: true}
95	case processStrategyOpenCode:
96		if id != "" {
97			return processRestoreDecision{Command: shellQuoteArgvMust([]string{"opencode", "--session", id}), Restore: true}
98		}
99		return processRestoreDecision{Command: "opencode --continue", Restore: true}
100	case processStrategyCodex:
101		if id != "" {
102			return processRestoreDecision{Command: shellQuoteArgvMust([]string{"codex", "resume", id}), Restore: true}
103		}
104		return processRestoreDecision{Command: "codex resume --last", Restore: true}
105	}
106
107	command, ok := shellQuoteArgv(proc.Argv)
108	if !ok {
109		return processRestoreDecision{Reason: "empty_command"}
110	}
111	return processRestoreDecision{Command: command, Restore: true}
112}
113
114func normalizeProcessStrategy(strategy string, argv []string) string {
115	switch strategy {
116	case processStrategyClaude, processStrategyCodex, processStrategyOpenCode, processStrategyPi:
117		return strategy
118	case "", processStrategyGeneric:
119		return detectProcessStrategy(argv)
120	default:
121		return processStrategyGeneric
122	}
123}
124
125func processAllowlistKey(strategy string, argv []string) string {
126	switch strategy {
127	case processStrategyClaude, processStrategyCodex, processStrategyOpenCode, processStrategyPi:
128		return strategy
129	default:
130		return filepath.Base(argv[0])
131	}
132}
133
134func containsControl(s string) bool {
135	return strings.IndexFunc(s, unicode.IsControl) >= 0
136}
137
138func shellQuoteArgvMust(argv []string) string {
139	command, _ := shellQuoteArgv(argv)
140	return command
141}
142
143func shellQuoteArgv(argv []string) (string, bool) {
144	if len(argv) == 0 {
145		return "", false
146	}
147	quoted := make([]string, 0, len(argv))
148	for _, arg := range argv {
149		quoted = append(quoted, shellQuote(arg))
150	}
151	return strings.Join(quoted, " "), true
152}
153
154func shellQuote(s string) string {
155	if s == "" {
156		return "''"
157	}
158	if strings.IndexFunc(s, func(r rune) bool {
159		return r != '-' && r != '_' && r != '.' && r != '/' && r != ':' && r != '+' && r != '=' && !unicode.IsLetter(r) && !unicode.IsDigit(r)
160	}) == -1 {
161		return s
162	}
163	return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
164}

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 12

Tags 11

↑↓ moveenter openesc close