letmecook 0.0.1 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +120 -0
- package/bin.js +3 -0
- package/index.ts +234 -0
- package/package.json +42 -5
- package/src/agents-md.ts +115 -0
- package/src/flows/add-repos.ts +57 -0
- package/src/flows/add-skills.ts +57 -0
- package/src/flows/edit-session.ts +107 -0
- package/src/flows/index.ts +5 -0
- package/src/flows/new-session.ts +182 -0
- package/src/flows/resume-session.ts +231 -0
- package/src/git.ts +256 -0
- package/src/naming.ts +57 -0
- package/src/opencode-integration.ts +20 -0
- package/src/repo-history.ts +82 -0
- package/src/sessions.ts +217 -0
- package/src/skills.ts +49 -0
- package/src/tui-mode.ts +184 -0
- package/src/types.ts +80 -0
- package/src/ui/add-repos.ts +396 -0
- package/src/ui/agent-proposal.ts +80 -0
- package/src/ui/common/repo-formatter.ts +45 -0
- package/src/ui/confirm-delete.ts +95 -0
- package/src/ui/conflict.ts +121 -0
- package/src/ui/exit.ts +175 -0
- package/src/ui/list.ts +112 -0
- package/src/ui/main-menu.ts +155 -0
- package/src/ui/new-session.ts +99 -0
- package/src/ui/progress.ts +191 -0
- package/src/ui/reclone-prompt.ts +93 -0
- package/src/ui/renderer.ts +108 -0
- package/src/ui/session-actions.ts +109 -0
- package/src/ui/session-details.ts +77 -0
- package/src/ui/session-options.ts +41 -0
- package/src/ui/session-settings.ts +363 -0
- package/src/ui/skills.ts +185 -0
- package/src/utils/stream.ts +108 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type CliRenderer,
|
|
3
|
+
TextRenderable,
|
|
4
|
+
SelectRenderable,
|
|
5
|
+
SelectRenderableEvents,
|
|
6
|
+
type KeyEvent,
|
|
7
|
+
} from "@opentui/core";
|
|
8
|
+
import { createBaseLayout, clearLayout } from "./renderer";
|
|
9
|
+
import type { Session } from "../types";
|
|
10
|
+
import { formatRepoList } from "./common/repo-formatter";
|
|
11
|
+
|
|
12
|
+
export type SessionAction = "continue" | "add-repos" | "exit";
|
|
13
|
+
|
|
14
|
+
export function showSessionActions(
|
|
15
|
+
renderer: CliRenderer,
|
|
16
|
+
session: Session,
|
|
17
|
+
): Promise<SessionAction> {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
clearLayout(renderer);
|
|
20
|
+
|
|
21
|
+
const { content } = createBaseLayout(renderer, "Session paused");
|
|
22
|
+
|
|
23
|
+
// Session info
|
|
24
|
+
const sessionInfo = new TextRenderable(renderer, {
|
|
25
|
+
id: "session-info",
|
|
26
|
+
content: `Session: ${session.name}`,
|
|
27
|
+
fg: "#38bdf8",
|
|
28
|
+
marginBottom: 1,
|
|
29
|
+
});
|
|
30
|
+
content.add(sessionInfo);
|
|
31
|
+
|
|
32
|
+
// Show repos
|
|
33
|
+
const reposText = formatRepoList(session.repos, { showMarkers: true, prefix: " " });
|
|
34
|
+
const reposInfo = new TextRenderable(renderer, {
|
|
35
|
+
id: "repos-info",
|
|
36
|
+
content: `Repositories:\n${reposText}`,
|
|
37
|
+
fg: "#94a3b8",
|
|
38
|
+
marginBottom: 1,
|
|
39
|
+
});
|
|
40
|
+
content.add(reposInfo);
|
|
41
|
+
|
|
42
|
+
// Question
|
|
43
|
+
const question = new TextRenderable(renderer, {
|
|
44
|
+
id: "question",
|
|
45
|
+
content: "What would you like to do?",
|
|
46
|
+
fg: "#e2e8f0",
|
|
47
|
+
});
|
|
48
|
+
content.add(question);
|
|
49
|
+
|
|
50
|
+
// Options
|
|
51
|
+
const select = new SelectRenderable(renderer, {
|
|
52
|
+
id: "action-select",
|
|
53
|
+
width: 40,
|
|
54
|
+
height: 3,
|
|
55
|
+
options: [
|
|
56
|
+
{ name: "Continue session", description: "Re-launch claude", value: "continue" },
|
|
57
|
+
{
|
|
58
|
+
name: "Add repositories",
|
|
59
|
+
description: "Clone more repos to this session",
|
|
60
|
+
value: "add-repos",
|
|
61
|
+
},
|
|
62
|
+
{ name: "Exit session", description: "Choose to keep or delete session", value: "exit" },
|
|
63
|
+
],
|
|
64
|
+
showDescription: true,
|
|
65
|
+
backgroundColor: "transparent",
|
|
66
|
+
focusedBackgroundColor: "transparent",
|
|
67
|
+
selectedBackgroundColor: "#334155",
|
|
68
|
+
textColor: "#e2e8f0",
|
|
69
|
+
selectedTextColor: "#38bdf8",
|
|
70
|
+
descriptionColor: "#64748b",
|
|
71
|
+
selectedDescriptionColor: "#94a3b8",
|
|
72
|
+
marginTop: 1,
|
|
73
|
+
});
|
|
74
|
+
content.add(select);
|
|
75
|
+
|
|
76
|
+
// Instructions
|
|
77
|
+
const instructions = new TextRenderable(renderer, {
|
|
78
|
+
id: "instructions",
|
|
79
|
+
content: "\n[Enter] Select [Esc] Exit session",
|
|
80
|
+
fg: "#64748b",
|
|
81
|
+
marginTop: 1,
|
|
82
|
+
});
|
|
83
|
+
content.add(instructions);
|
|
84
|
+
|
|
85
|
+
select.focus();
|
|
86
|
+
|
|
87
|
+
const handleSelect = (_index: number, option: { value: string }) => {
|
|
88
|
+
cleanup();
|
|
89
|
+
resolve(option.value as SessionAction);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleKeypress = (key: KeyEvent) => {
|
|
93
|
+
if (key.name === "escape") {
|
|
94
|
+
cleanup();
|
|
95
|
+
resolve("exit");
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const cleanup = () => {
|
|
100
|
+
select.off(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
101
|
+
renderer.keyInput.off("keypress", handleKeypress);
|
|
102
|
+
select.blur();
|
|
103
|
+
clearLayout(renderer);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
107
|
+
renderer.keyInput.on("keypress", handleKeypress);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { type CliRenderer, TextRenderable, type KeyEvent } from "@opentui/core";
|
|
2
|
+
import { createBaseLayout, clearLayout } from "./renderer";
|
|
3
|
+
import type { Session } from "../types";
|
|
4
|
+
import { formatRepoList } from "./common/repo-formatter";
|
|
5
|
+
|
|
6
|
+
export type SessionDetailsAction = "resume" | "edit" | "add-repos" | "back";
|
|
7
|
+
|
|
8
|
+
export function showSessionDetails(
|
|
9
|
+
renderer: CliRenderer,
|
|
10
|
+
session: Session,
|
|
11
|
+
): Promise<SessionDetailsAction> {
|
|
12
|
+
return new Promise((resolve) => {
|
|
13
|
+
clearLayout(renderer);
|
|
14
|
+
|
|
15
|
+
const { content } = createBaseLayout(renderer, "Session details");
|
|
16
|
+
|
|
17
|
+
const sessionInfo = new TextRenderable(renderer, {
|
|
18
|
+
id: "session-info",
|
|
19
|
+
content: `Session: ${session.name}`,
|
|
20
|
+
fg: "#38bdf8",
|
|
21
|
+
marginBottom: 1,
|
|
22
|
+
});
|
|
23
|
+
content.add(sessionInfo);
|
|
24
|
+
|
|
25
|
+
const goalText = session.goal ? session.goal : "(none)";
|
|
26
|
+
const goalInfo = new TextRenderable(renderer, {
|
|
27
|
+
id: "goal-info",
|
|
28
|
+
content: `Goal: ${goalText}`,
|
|
29
|
+
fg: "#94a3b8",
|
|
30
|
+
marginBottom: 1,
|
|
31
|
+
});
|
|
32
|
+
content.add(goalInfo);
|
|
33
|
+
|
|
34
|
+
const reposText = formatRepoList(session.repos, { prefix: " " });
|
|
35
|
+
|
|
36
|
+
const reposInfo = new TextRenderable(renderer, {
|
|
37
|
+
id: "repos-info",
|
|
38
|
+
content: `Repositories:\n${reposText || " (none)"}`,
|
|
39
|
+
fg: "#94a3b8",
|
|
40
|
+
marginBottom: 1,
|
|
41
|
+
});
|
|
42
|
+
content.add(reposInfo);
|
|
43
|
+
|
|
44
|
+
const instructions = new TextRenderable(renderer, {
|
|
45
|
+
id: "instructions",
|
|
46
|
+
content: "\n[Enter] Resume [e] Edit settings [Esc] Back",
|
|
47
|
+
fg: "#64748b",
|
|
48
|
+
marginTop: 1,
|
|
49
|
+
});
|
|
50
|
+
content.add(instructions);
|
|
51
|
+
|
|
52
|
+
const handleKeypress = (key: KeyEvent) => {
|
|
53
|
+
if (key.name === "return" || key.name === "enter") {
|
|
54
|
+
cleanup();
|
|
55
|
+
resolve("resume");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (key.name === "e") {
|
|
60
|
+
cleanup();
|
|
61
|
+
resolve("edit");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (key.name === "escape") {
|
|
66
|
+
cleanup();
|
|
67
|
+
resolve("back");
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const cleanup = () => {
|
|
72
|
+
renderer.keyInput.off("keypress", handleKeypress);
|
|
73
|
+
clearLayout(renderer);
|
|
74
|
+
};
|
|
75
|
+
renderer.keyInput.on("keypress", handleKeypress);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Session } from "../types";
|
|
2
|
+
|
|
3
|
+
export interface SessionOption {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
value: Session;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function formatTimeAgo(date: string): string {
|
|
10
|
+
const now = new Date();
|
|
11
|
+
const then = new Date(date);
|
|
12
|
+
const diffMs = now.getTime() - then.getTime();
|
|
13
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
14
|
+
const diffHours = Math.floor(diffMins / 60);
|
|
15
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
16
|
+
|
|
17
|
+
if (diffMins < 1) return "just now";
|
|
18
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
19
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
20
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
21
|
+
return then.toLocaleDateString();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function buildSessionOptions(sessions: Session[]): SessionOption[] {
|
|
25
|
+
return sessions.map((session) => {
|
|
26
|
+
let description = session.repos.map((repo) => repo.name).join(" | ");
|
|
27
|
+
const time = formatTimeAgo(session.lastAccessed);
|
|
28
|
+
|
|
29
|
+
if (session.goal) {
|
|
30
|
+
const truncatedGoal =
|
|
31
|
+
session.goal.length > 60 ? `${session.goal.slice(0, 60)}...` : session.goal;
|
|
32
|
+
description += `\n "${truncatedGoal}"`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
name: `${session.name} (${time})`,
|
|
37
|
+
description,
|
|
38
|
+
value: session,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { type CliRenderer, TextRenderable, InputRenderable, type KeyEvent } from "@opentui/core";
|
|
2
|
+
import { createBaseLayout, clearLayout } from "./renderer";
|
|
3
|
+
import type { Session, RepoSpec } from "../types";
|
|
4
|
+
import { formatRepoString } from "./common/repo-formatter";
|
|
5
|
+
|
|
6
|
+
export interface SessionSettingsResult {
|
|
7
|
+
action: "saved" | "add-repos" | "add-skills" | "cancel";
|
|
8
|
+
session: Session;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type EditMode = "repos" | "goal";
|
|
12
|
+
type SelectionTarget = "goal" | "repo" | "skill";
|
|
13
|
+
|
|
14
|
+
export function showSessionSettings(
|
|
15
|
+
renderer: CliRenderer,
|
|
16
|
+
session: Session,
|
|
17
|
+
): Promise<SessionSettingsResult> {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
clearLayout(renderer);
|
|
20
|
+
|
|
21
|
+
const { content } = createBaseLayout(renderer, "Edit session settings");
|
|
22
|
+
|
|
23
|
+
const sessionInfo = new TextRenderable(renderer, {
|
|
24
|
+
id: "session-info",
|
|
25
|
+
content: `Session: ${session.name}`,
|
|
26
|
+
fg: "#38bdf8",
|
|
27
|
+
marginBottom: 1,
|
|
28
|
+
});
|
|
29
|
+
content.add(sessionInfo);
|
|
30
|
+
|
|
31
|
+
const goalLabel = new TextRenderable(renderer, {
|
|
32
|
+
id: "goal-label",
|
|
33
|
+
content: "Goal:",
|
|
34
|
+
fg: "#e2e8f0",
|
|
35
|
+
marginBottom: 0,
|
|
36
|
+
});
|
|
37
|
+
content.add(goalLabel);
|
|
38
|
+
|
|
39
|
+
const goalInput = new InputRenderable(renderer, {
|
|
40
|
+
id: "goal-input",
|
|
41
|
+
width: 60,
|
|
42
|
+
height: 1,
|
|
43
|
+
placeholder: "Add a goal for this session",
|
|
44
|
+
placeholderColor: "#64748b",
|
|
45
|
+
backgroundColor: "#334155",
|
|
46
|
+
textColor: "#f8fafc",
|
|
47
|
+
cursorColor: "#38bdf8",
|
|
48
|
+
marginTop: 1,
|
|
49
|
+
});
|
|
50
|
+
goalInput.value = session.goal ?? "";
|
|
51
|
+
content.add(goalInput);
|
|
52
|
+
|
|
53
|
+
const reposLabel = new TextRenderable(renderer, {
|
|
54
|
+
id: "repos-label",
|
|
55
|
+
content: "\nRepositories:",
|
|
56
|
+
fg: "#e2e8f0",
|
|
57
|
+
marginTop: 1,
|
|
58
|
+
marginBottom: 0,
|
|
59
|
+
});
|
|
60
|
+
content.add(reposLabel);
|
|
61
|
+
|
|
62
|
+
const reposList = new TextRenderable(renderer, {
|
|
63
|
+
id: "repos-list",
|
|
64
|
+
content: "(none)",
|
|
65
|
+
fg: "#94a3b8",
|
|
66
|
+
marginTop: 0,
|
|
67
|
+
});
|
|
68
|
+
content.add(reposList);
|
|
69
|
+
|
|
70
|
+
const skillsLabel = new TextRenderable(renderer, {
|
|
71
|
+
id: "skills-label",
|
|
72
|
+
content: "\nSkills:",
|
|
73
|
+
fg: "#e2e8f0",
|
|
74
|
+
marginTop: 1,
|
|
75
|
+
marginBottom: 0,
|
|
76
|
+
});
|
|
77
|
+
content.add(skillsLabel);
|
|
78
|
+
|
|
79
|
+
const skillsList = new TextRenderable(renderer, {
|
|
80
|
+
id: "skills-list",
|
|
81
|
+
content: "(none)",
|
|
82
|
+
fg: "#94a3b8",
|
|
83
|
+
marginTop: 0,
|
|
84
|
+
});
|
|
85
|
+
content.add(skillsList);
|
|
86
|
+
|
|
87
|
+
const statusText = new TextRenderable(renderer, {
|
|
88
|
+
id: "status",
|
|
89
|
+
content: "",
|
|
90
|
+
fg: "#64748b",
|
|
91
|
+
marginTop: 1,
|
|
92
|
+
});
|
|
93
|
+
content.add(statusText);
|
|
94
|
+
|
|
95
|
+
const instructions = new TextRenderable(renderer, {
|
|
96
|
+
id: "instructions",
|
|
97
|
+
content: "",
|
|
98
|
+
fg: "#64748b",
|
|
99
|
+
marginTop: 1,
|
|
100
|
+
});
|
|
101
|
+
content.add(instructions);
|
|
102
|
+
|
|
103
|
+
let mode: EditMode = "repos";
|
|
104
|
+
let selectedTarget: SelectionTarget = "goal";
|
|
105
|
+
let selectedRepoIndex = 0;
|
|
106
|
+
let selectedSkillIndex = 0;
|
|
107
|
+
const updatedRepos: RepoSpec[] = session.repos.map((repo) => ({ ...repo }));
|
|
108
|
+
const updatedSkills = (session.skills || []).slice();
|
|
109
|
+
let updatedGoal = session.goal ?? "";
|
|
110
|
+
|
|
111
|
+
const updateReposList = () => {
|
|
112
|
+
if (updatedRepos.length === 0) {
|
|
113
|
+
reposList.content = "(none)";
|
|
114
|
+
reposList.fg = "#64748b";
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
reposList.content = updatedRepos
|
|
119
|
+
.map((repo, index) => {
|
|
120
|
+
const marker = selectedTarget === "repo" && index === selectedRepoIndex ? "▶" : " ";
|
|
121
|
+
return `${marker} ${formatRepoString(repo)}`;
|
|
122
|
+
})
|
|
123
|
+
.join("\n");
|
|
124
|
+
reposList.fg = "#94a3b8";
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const updateSkillsList = () => {
|
|
128
|
+
if (updatedSkills.length === 0) {
|
|
129
|
+
skillsList.content = "(none)";
|
|
130
|
+
skillsList.fg = "#64748b";
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
skillsList.content = updatedSkills
|
|
135
|
+
.map((skill, index) => {
|
|
136
|
+
const marker = selectedTarget === "skill" && index === selectedSkillIndex ? "▶" : " ";
|
|
137
|
+
return `${marker} ${skill}`;
|
|
138
|
+
})
|
|
139
|
+
.join("\n");
|
|
140
|
+
skillsList.fg = "#94a3b8";
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const updateInstructions = () => {
|
|
144
|
+
if (selectedTarget === "goal") {
|
|
145
|
+
instructions.content = "\n[Enter] Edit goal [s] Save [Esc] Cancel";
|
|
146
|
+
} else if (selectedTarget === "repo") {
|
|
147
|
+
instructions.content =
|
|
148
|
+
"\n[r] Toggle read-only [l] Toggle latest [a] Add repos [k] Add skills [s] Save [Esc] Cancel";
|
|
149
|
+
} else if (selectedTarget === "skill") {
|
|
150
|
+
instructions.content =
|
|
151
|
+
"\n[x] Remove skill [a] Add repos [k] Add skills [s] Save [Esc] Cancel";
|
|
152
|
+
} else {
|
|
153
|
+
instructions.content =
|
|
154
|
+
"\n[r] Toggle read-only [l] Toggle latest [a] Add repos [k] Add skills [s] Save [Esc] Cancel";
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const updateGoalLabel = () => {
|
|
159
|
+
if (selectedTarget === "goal") {
|
|
160
|
+
goalLabel.content = "▶ Goal:";
|
|
161
|
+
} else {
|
|
162
|
+
goalLabel.content = "Goal:";
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const updateReposLabel = () => {
|
|
167
|
+
if (selectedTarget === "repo") {
|
|
168
|
+
reposLabel.content = "▶ Repositories:";
|
|
169
|
+
} else {
|
|
170
|
+
reposLabel.content = "Repositories:";
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const updateSkillsLabel = () => {
|
|
175
|
+
if (selectedTarget === "skill") {
|
|
176
|
+
skillsLabel.content = "▶ Skills:";
|
|
177
|
+
} else {
|
|
178
|
+
skillsLabel.content = "Skills:";
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const updateMode = (nextMode: EditMode) => {
|
|
183
|
+
mode = nextMode;
|
|
184
|
+
if (mode === "goal") {
|
|
185
|
+
goalInput.focus();
|
|
186
|
+
statusText.content = "Editing goal. Press Enter to return.";
|
|
187
|
+
statusText.fg = "#38bdf8";
|
|
188
|
+
} else {
|
|
189
|
+
goalInput.blur();
|
|
190
|
+
statusText.content = "";
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const handleKeypress = (key: KeyEvent) => {
|
|
195
|
+
if (mode === "goal") {
|
|
196
|
+
if (key.name === "return" || key.name === "enter") {
|
|
197
|
+
updatedGoal = goalInput.value.trim();
|
|
198
|
+
updateMode("repos");
|
|
199
|
+
} else if (key.name === "escape") {
|
|
200
|
+
goalInput.value = updatedGoal;
|
|
201
|
+
updateMode("repos");
|
|
202
|
+
}
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (key.name === "escape") {
|
|
207
|
+
cleanup();
|
|
208
|
+
resolve({ action: "cancel", session });
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (key.name === "up" || key.name === "k") {
|
|
213
|
+
if (selectedTarget === "skill" && selectedSkillIndex > 0) {
|
|
214
|
+
selectedSkillIndex = Math.max(0, selectedSkillIndex - 1);
|
|
215
|
+
} else if (selectedTarget === "repo" && selectedRepoIndex > 0) {
|
|
216
|
+
selectedRepoIndex = Math.max(0, selectedRepoIndex - 1);
|
|
217
|
+
} else if (selectedTarget === "repo") {
|
|
218
|
+
selectedTarget = "goal";
|
|
219
|
+
} else if (selectedTarget === "goal" && updatedSkills.length > 0) {
|
|
220
|
+
selectedTarget = "skill";
|
|
221
|
+
selectedSkillIndex = updatedSkills.length - 1;
|
|
222
|
+
}
|
|
223
|
+
updateGoalLabel();
|
|
224
|
+
updateReposLabel();
|
|
225
|
+
updateSkillsLabel();
|
|
226
|
+
updateInstructions();
|
|
227
|
+
updateReposList();
|
|
228
|
+
updateSkillsList();
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (key.name === "down" || key.name === "j") {
|
|
233
|
+
if (selectedTarget === "goal") {
|
|
234
|
+
if (updatedRepos.length > 0) {
|
|
235
|
+
selectedTarget = "repo";
|
|
236
|
+
selectedRepoIndex = 0;
|
|
237
|
+
} else if (updatedSkills.length > 0) {
|
|
238
|
+
selectedTarget = "skill";
|
|
239
|
+
selectedSkillIndex = 0;
|
|
240
|
+
}
|
|
241
|
+
} else if (selectedTarget === "repo") {
|
|
242
|
+
if (selectedRepoIndex < updatedRepos.length - 1) {
|
|
243
|
+
selectedRepoIndex = Math.min(updatedRepos.length - 1, selectedRepoIndex + 1);
|
|
244
|
+
} else if (updatedSkills.length > 0) {
|
|
245
|
+
selectedTarget = "skill";
|
|
246
|
+
selectedSkillIndex = 0;
|
|
247
|
+
}
|
|
248
|
+
} else if (selectedTarget === "skill") {
|
|
249
|
+
selectedSkillIndex = Math.min(updatedSkills.length - 1, selectedSkillIndex + 1);
|
|
250
|
+
}
|
|
251
|
+
updateGoalLabel();
|
|
252
|
+
updateReposLabel();
|
|
253
|
+
updateSkillsLabel();
|
|
254
|
+
updateInstructions();
|
|
255
|
+
updateReposList();
|
|
256
|
+
updateSkillsList();
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if ((key.name === "return" || key.name === "enter") && selectedTarget === "goal") {
|
|
261
|
+
updateMode("goal");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (key.name === "a") {
|
|
266
|
+
updatedGoal = goalInput.value.trim();
|
|
267
|
+
const updatedSession: Session = {
|
|
268
|
+
...session,
|
|
269
|
+
repos: updatedRepos,
|
|
270
|
+
skills: updatedSkills,
|
|
271
|
+
goal: updatedGoal || undefined,
|
|
272
|
+
};
|
|
273
|
+
cleanup();
|
|
274
|
+
resolve({ action: "add-repos", session: updatedSession });
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (key.name === "k") {
|
|
279
|
+
updatedGoal = goalInput.value.trim();
|
|
280
|
+
const updatedSession: Session = {
|
|
281
|
+
...session,
|
|
282
|
+
repos: updatedRepos,
|
|
283
|
+
skills: updatedSkills,
|
|
284
|
+
goal: updatedGoal || undefined,
|
|
285
|
+
};
|
|
286
|
+
cleanup();
|
|
287
|
+
resolve({ action: "add-skills", session: updatedSession });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (key.name === "x" && selectedTarget === "skill") {
|
|
292
|
+
const skill = updatedSkills[selectedSkillIndex];
|
|
293
|
+
if (skill) {
|
|
294
|
+
updatedSkills.splice(selectedSkillIndex, 1);
|
|
295
|
+
if (selectedSkillIndex >= updatedSkills.length) {
|
|
296
|
+
selectedSkillIndex = Math.max(0, updatedSkills.length - 1);
|
|
297
|
+
}
|
|
298
|
+
if (updatedSkills.length === 0) {
|
|
299
|
+
selectedTarget = updatedRepos.length > 0 ? "repo" : "goal";
|
|
300
|
+
}
|
|
301
|
+
updateGoalLabel();
|
|
302
|
+
updateReposLabel();
|
|
303
|
+
updateSkillsLabel();
|
|
304
|
+
updateInstructions();
|
|
305
|
+
updateReposList();
|
|
306
|
+
updateSkillsList();
|
|
307
|
+
}
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (key.name === "r") {
|
|
312
|
+
const repo = updatedRepos[selectedRepoIndex];
|
|
313
|
+
if (repo) {
|
|
314
|
+
repo.readOnly = !repo.readOnly;
|
|
315
|
+
if (!repo.readOnly) {
|
|
316
|
+
repo.latest = false;
|
|
317
|
+
}
|
|
318
|
+
updateReposList();
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (key.name === "l") {
|
|
324
|
+
const repo = updatedRepos[selectedRepoIndex];
|
|
325
|
+
if (repo) {
|
|
326
|
+
repo.latest = !repo.latest;
|
|
327
|
+
if (repo.latest) {
|
|
328
|
+
repo.readOnly = true;
|
|
329
|
+
}
|
|
330
|
+
updateReposList();
|
|
331
|
+
}
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (key.name === "s") {
|
|
336
|
+
updatedGoal = goalInput.value.trim();
|
|
337
|
+
const updatedSession: Session = {
|
|
338
|
+
...session,
|
|
339
|
+
repos: updatedRepos,
|
|
340
|
+
skills: updatedSkills.length > 0 ? updatedSkills : undefined,
|
|
341
|
+
goal: updatedGoal || undefined,
|
|
342
|
+
};
|
|
343
|
+
cleanup();
|
|
344
|
+
resolve({ action: "saved", session: updatedSession });
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const cleanup = () => {
|
|
349
|
+
renderer.keyInput.off("keypress", handleKeypress);
|
|
350
|
+
goalInput.blur();
|
|
351
|
+
clearLayout(renderer);
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
updateGoalLabel();
|
|
355
|
+
updateReposLabel();
|
|
356
|
+
updateSkillsLabel();
|
|
357
|
+
updateInstructions();
|
|
358
|
+
updateReposList();
|
|
359
|
+
updateSkillsList();
|
|
360
|
+
updateMode("repos");
|
|
361
|
+
renderer.keyInput.on("keypress", handleKeypress);
|
|
362
|
+
});
|
|
363
|
+
}
|