letmecook 0.0.13 → 0.0.15

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.
@@ -2,6 +2,8 @@ import { type CliRenderer, TextRenderable, InputRenderable, type KeyEvent } from
2
2
  import { createBaseLayout, clearLayout } from "./renderer";
3
3
  import type { Session, RepoSpec } from "../types";
4
4
  import { formatRepoString } from "./common/repo-formatter";
5
+ import { showFooter, hideFooter } from "./common/footer";
6
+ import { isEnter, isEscape, isTab, isArrowUp, isArrowDown } from "./common/keyboard";
5
7
 
6
8
  export interface SessionSettingsResult {
7
9
  action: "saved" | "add-repos" | "add-skills" | "cancel";
@@ -140,21 +142,6 @@ export function showSessionSettings(
140
142
  skillsList.fg = "#94a3b8";
141
143
  };
142
144
 
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
145
  const updateGoalLabel = () => {
159
146
  if (selectedTarget === "goal") {
160
147
  goalLabel.content = "▶ Goal:";
@@ -191,77 +178,120 @@ export function showSessionSettings(
191
178
  }
192
179
  };
193
180
 
181
+ const switchToNextSection = () => {
182
+ if (selectedTarget === "goal") {
183
+ if (updatedRepos.length > 0) {
184
+ selectedTarget = "repo";
185
+ selectedRepoIndex = 0;
186
+ } else if (updatedSkills.length > 0) {
187
+ selectedTarget = "skill";
188
+ selectedSkillIndex = 0;
189
+ }
190
+ } else if (selectedTarget === "repo") {
191
+ if (updatedSkills.length > 0) {
192
+ selectedTarget = "skill";
193
+ selectedSkillIndex = 0;
194
+ } else {
195
+ selectedTarget = "goal";
196
+ }
197
+ } else if (selectedTarget === "skill") {
198
+ selectedTarget = "goal";
199
+ }
200
+ updateGoalLabel();
201
+ updateReposLabel();
202
+ updateSkillsLabel();
203
+ updateFooter();
204
+ };
205
+
206
+ const updateFooter = () => {
207
+ const customActions: string[] = [];
208
+ if (selectedTarget === "goal") {
209
+ customActions.push("Enter Edit");
210
+ } else if (selectedTarget === "repo") {
211
+ customActions.push("r Toggle RO", "l Toggle Latest", "a Add repos");
212
+ } else if (selectedTarget === "skill") {
213
+ customActions.push("x Remove", "a Add repos");
214
+ }
215
+ customActions.push("+ Add skills", "Enter Save");
216
+
217
+ hideFooter(renderer);
218
+ showFooter(renderer, content, {
219
+ navigate: true,
220
+ select: false,
221
+ back: true,
222
+ custom: ["Tab Switch", ...customActions],
223
+ });
224
+ };
225
+
194
226
  const handleKeypress = (key: KeyEvent) => {
195
227
  if (mode === "goal") {
196
- if (key.name === "return" || key.name === "enter") {
228
+ if (isEnter(key)) {
197
229
  updatedGoal = goalInput.value.trim();
198
230
  updateMode("repos");
199
- } else if (key.name === "escape") {
231
+ updateFooter();
232
+ } else if (isEscape(key)) {
200
233
  goalInput.value = updatedGoal;
201
234
  updateMode("repos");
235
+ updateFooter();
202
236
  }
203
237
  return;
204
238
  }
205
239
 
206
- if (key.name === "escape") {
240
+ if (isEscape(key)) {
207
241
  cleanup();
208
242
  resolve({ action: "cancel", session });
209
243
  return;
210
244
  }
211
245
 
212
- if (key.name === "up" || key.name === "k") {
246
+ // Tab to switch sections
247
+ if (isTab(key)) {
248
+ switchToNextSection();
249
+ return;
250
+ }
251
+
252
+ // Arrow keys for navigation within sections
253
+ if (isArrowUp(key)) {
213
254
  if (selectedTarget === "skill" && selectedSkillIndex > 0) {
214
255
  selectedSkillIndex = Math.max(0, selectedSkillIndex - 1);
215
256
  } else if (selectedTarget === "repo" && selectedRepoIndex > 0) {
216
257
  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
258
  }
223
- updateGoalLabel();
224
- updateReposLabel();
225
- updateSkillsLabel();
226
- updateInstructions();
227
259
  updateReposList();
228
260
  updateSkillsList();
229
261
  return;
230
262
  }
231
263
 
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") {
264
+ if (isArrowDown(key)) {
265
+ if (selectedTarget === "repo" && selectedRepoIndex < updatedRepos.length - 1) {
266
+ selectedRepoIndex = Math.min(updatedRepos.length - 1, selectedRepoIndex + 1);
267
+ } else if (selectedTarget === "skill" && selectedSkillIndex < updatedSkills.length - 1) {
249
268
  selectedSkillIndex = Math.min(updatedSkills.length - 1, selectedSkillIndex + 1);
250
269
  }
251
- updateGoalLabel();
252
- updateReposLabel();
253
- updateSkillsLabel();
254
- updateInstructions();
255
270
  updateReposList();
256
271
  updateSkillsList();
257
272
  return;
258
273
  }
259
274
 
260
- if ((key.name === "return" || key.name === "enter") && selectedTarget === "goal") {
261
- updateMode("goal");
275
+ // Enter to edit goal or save
276
+ if (isEnter(key)) {
277
+ if (selectedTarget === "goal") {
278
+ updateMode("goal");
279
+ } else {
280
+ // Save changes
281
+ updatedGoal = goalInput.value.trim();
282
+ const updatedSession: Session = {
283
+ ...session,
284
+ repos: updatedRepos,
285
+ skills: updatedSkills.length > 0 ? updatedSkills : undefined,
286
+ goal: updatedGoal || undefined,
287
+ };
288
+ cleanup();
289
+ resolve({ action: "saved", session: updatedSession });
290
+ }
262
291
  return;
263
292
  }
264
293
 
294
+ // Hotkeys
265
295
  if (key.name === "a") {
266
296
  updatedGoal = goalInput.value.trim();
267
297
  const updatedSession: Session = {
@@ -275,7 +305,8 @@ export function showSessionSettings(
275
305
  return;
276
306
  }
277
307
 
278
- if (key.name === "k") {
308
+ if (key.name === "+" || key.name === "=") {
309
+ // + or = for add skills
279
310
  updatedGoal = goalInput.value.trim();
280
311
  const updatedSession: Session = {
281
312
  ...session,
@@ -297,18 +328,18 @@ export function showSessionSettings(
297
328
  }
298
329
  if (updatedSkills.length === 0) {
299
330
  selectedTarget = updatedRepos.length > 0 ? "repo" : "goal";
331
+ updateGoalLabel();
332
+ updateReposLabel();
333
+ updateSkillsLabel();
334
+ updateFooter();
300
335
  }
301
- updateGoalLabel();
302
- updateReposLabel();
303
- updateSkillsLabel();
304
- updateInstructions();
305
336
  updateReposList();
306
337
  updateSkillsList();
307
338
  }
308
339
  return;
309
340
  }
310
341
 
311
- if (key.name === "r") {
342
+ if (key.name === "r" && selectedTarget === "repo") {
312
343
  const repo = updatedRepos[selectedRepoIndex];
313
344
  if (repo) {
314
345
  repo.readOnly = !repo.readOnly;
@@ -320,7 +351,7 @@ export function showSessionSettings(
320
351
  return;
321
352
  }
322
353
 
323
- if (key.name === "l") {
354
+ if (key.name === "l" && selectedTarget === "repo") {
324
355
  const repo = updatedRepos[selectedRepoIndex];
325
356
  if (repo) {
326
357
  repo.latest = !repo.latest;
@@ -331,33 +362,22 @@ export function showSessionSettings(
331
362
  }
332
363
  return;
333
364
  }
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
365
  };
347
366
 
348
367
  const cleanup = () => {
349
368
  renderer.keyInput.off("keypress", handleKeypress);
350
369
  goalInput.blur();
370
+ hideFooter(renderer);
351
371
  clearLayout(renderer);
352
372
  };
353
373
 
354
374
  updateGoalLabel();
355
375
  updateReposLabel();
356
376
  updateSkillsLabel();
357
- updateInstructions();
358
377
  updateReposList();
359
378
  updateSkillsList();
360
379
  updateMode("repos");
380
+ updateFooter();
361
381
  renderer.keyInput.on("keypress", handleKeypress);
362
382
  });
363
383
  }
package/src/ui/skills.ts CHANGED
@@ -1,12 +1,7 @@
1
- import {
2
- type CliRenderer,
3
- TextRenderable,
4
- InputRenderable,
5
- SelectRenderable,
6
- SelectRenderableEvents,
7
- type KeyEvent,
8
- } from "@opentui/core";
1
+ import { type CliRenderer, TextRenderable, InputRenderable, type KeyEvent } from "@opentui/core";
9
2
  import { createBaseLayout, clearLayout } from "./renderer";
3
+ import { showFooter, hideFooter } from "./common/footer";
4
+ import { isEnter, isEscape } from "./common/keyboard";
10
5
 
11
6
  export interface SkillsPromptResult {
12
7
  skills: string[];
@@ -44,93 +39,56 @@ export async function showSkillsPrompt(
44
39
  });
45
40
  content.add(input);
46
41
 
47
- const existingSkillsText = new TextRenderable(renderer, {
48
- id: "existing-skills",
49
- content: "",
50
- fg: "#94a3b8",
42
+ const skillsLabel = new TextRenderable(renderer, {
43
+ id: "skills-label",
44
+ content: "\nAdded skills:",
45
+ fg: "#e2e8f0",
51
46
  marginTop: 1,
52
47
  marginBottom: 0,
53
48
  });
54
- content.add(existingSkillsText);
49
+ content.add(skillsLabel);
55
50
 
56
- const confirmation = new SelectRenderable(renderer, {
57
- id: "confirmation-select",
58
- width: 40,
59
- height: 1,
60
- options: [
61
- { name: "Add another skill", description: "", value: "yes" },
62
- { name: "Done adding skills", description: "", value: "no" },
63
- ],
64
- showDescription: false,
65
- backgroundColor: "transparent",
66
- focusedBackgroundColor: "transparent",
67
- selectedBackgroundColor: "#334155",
68
- textColor: "#e2e8f0",
69
- selectedTextColor: "#38bdf8",
70
- descriptionColor: "#64748b",
71
- selectedDescriptionColor: "#94a3b8",
51
+ const existingSkillsText = new TextRenderable(renderer, {
52
+ id: "existing-skills",
53
+ content: "(none)",
54
+ fg: "#64748b",
72
55
  marginTop: 1,
56
+ marginBottom: 0,
73
57
  });
74
- content.add(confirmation);
58
+ content.add(existingSkillsText);
75
59
 
76
- const instructions = new TextRenderable(renderer, {
77
- id: "instructions",
78
- content: "\n[Enter] Add skill [Ctrl+D] Done [Esc] Cancel",
79
- fg: "#64748b",
80
- marginTop: 1,
60
+ const skillsCounter = new TextRenderable(renderer, {
61
+ id: "skills-counter",
62
+ content: "",
63
+ fg: "#94a3b8",
64
+ marginTop: 0,
81
65
  });
82
- content.add(instructions);
66
+ content.add(skillsCounter);
83
67
 
84
68
  const skills: string[] = [];
85
- let showingConfirmation = false;
86
69
 
87
70
  const updateExistingSkills = () => {
88
71
  if (skills.length === 0) {
89
- existingSkillsText.content = "";
72
+ existingSkillsText.content = "(none)";
73
+ existingSkillsText.fg = "#64748b";
74
+ skillsCounter.content = "";
90
75
  } else {
91
76
  const text = skills.map((skill) => ` ✓ ${skill}`).join("\n");
92
- existingSkillsText.content = `Added skills:\n${text}`;
77
+ existingSkillsText.content = text;
78
+ existingSkillsText.fg = "#94a3b8";
79
+ skillsCounter.content = `Added: ${skills.length} ${skills.length === 1 ? "skill" : "skills"}`;
93
80
  }
94
81
  };
95
82
 
96
83
  const cleanup = () => {
97
84
  renderer.keyInput.off("keypress", handleKeypress);
98
85
  input.blur();
99
- confirmation.off(SelectRenderableEvents.ITEM_SELECTED, handleConfirm);
100
- confirmation.blur();
86
+ hideFooter(renderer);
101
87
  clearLayout(renderer);
102
88
  };
103
89
 
104
90
  const handleKeypress = (key: KeyEvent) => {
105
- if (showingConfirmation) {
106
- if (key.name === "return" || key.name === "enter") {
107
- cleanup();
108
- resolve({
109
- skills,
110
- cancelled: false,
111
- });
112
- return;
113
- }
114
- if (key.name === "escape") {
115
- cleanup();
116
- resolve({
117
- skills,
118
- cancelled: false,
119
- });
120
- return;
121
- }
122
- if (key.name === "d" && key.ctrl) {
123
- cleanup();
124
- resolve({
125
- skills,
126
- cancelled: false,
127
- });
128
- return;
129
- }
130
- return;
131
- }
132
-
133
- if (key.name === "escape") {
91
+ if (isEscape(key)) {
134
92
  cleanup();
135
93
  resolve({
136
94
  skills: [],
@@ -139,47 +97,33 @@ export async function showSkillsPrompt(
139
97
  return;
140
98
  }
141
99
 
142
- if (key.name === "d" && key.ctrl) {
143
- cleanup();
144
- resolve({
145
- skills,
146
- cancelled: false,
147
- });
148
- return;
149
- }
150
-
151
- if (key.name === "return" || key.name === "enter") {
100
+ if (isEnter(key)) {
152
101
  const skill = input.value.trim();
153
102
  if (skill) {
103
+ // Add skill
154
104
  skills.push(skill);
155
105
  updateExistingSkills();
156
106
  input.value = "";
157
- showingConfirmation = true;
158
- confirmation.focus();
159
- instructions.content = "\n[Enter] Add another [Ctrl+D] Done [Esc] Cancel";
107
+ } else {
108
+ // Empty input = done (with or without skills)
109
+ cleanup();
110
+ resolve({
111
+ skills,
112
+ cancelled: false,
113
+ });
160
114
  }
161
- }
162
- };
163
-
164
- const handleConfirm = (_index: number, option: { value: string }) => {
165
- if (option.value === "yes") {
166
- showingConfirmation = false;
167
- input.focus();
168
- instructions.content = "\n[Enter] Add skill [Esc] Cancel";
169
115
  return;
170
116
  }
171
-
172
- cleanup();
173
- resolve({
174
- skills,
175
- cancelled: false,
176
- });
177
117
  };
178
118
 
179
119
  input.focus();
180
- confirmation.off(SelectRenderableEvents.ITEM_SELECTED, handleConfirm);
181
- renderer.keyInput.off("keypress", handleKeypress);
120
+ showFooter(renderer, content, {
121
+ navigate: false,
122
+ select: true,
123
+ back: true,
124
+ custom: ["Enter Add/Done"],
125
+ });
182
126
  renderer.keyInput.on("keypress", handleKeypress);
183
- confirmation.on(SelectRenderableEvents.ITEM_SELECTED, handleConfirm);
127
+ updateExistingSkills();
184
128
  });
185
129
  }