letmecook 0.0.13 → 0.0.14

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/src/ui/exit.ts CHANGED
@@ -1,7 +1,15 @@
1
- import { type CliRenderer, TextRenderable, type KeyEvent } from "@opentui/core";
1
+ import {
2
+ type CliRenderer,
3
+ TextRenderable,
4
+ SelectRenderable,
5
+ SelectRenderableEvents,
6
+ type KeyEvent,
7
+ } from "@opentui/core";
2
8
  import { createBaseLayout, clearLayout } from "./renderer";
3
9
  import type { Session, ExitChoice, RepoSpec } from "../types";
4
10
  import { sessionHasUncommittedChanges } from "../git";
11
+ import { showFooter, hideFooter } from "./common/footer";
12
+ import { isEscape } from "./common/keyboard";
5
13
 
6
14
  export function showExitPrompt(renderer: CliRenderer, session: Session): Promise<ExitChoice> {
7
15
  return new Promise((resolve) => {
@@ -23,47 +31,59 @@ export function showExitPrompt(renderer: CliRenderer, session: Session): Promise
23
31
  id: "question",
24
32
  content: "What would you like to do?",
25
33
  fg: "#e2e8f0",
34
+ marginBottom: 1,
26
35
  });
27
36
  content.add(question);
28
37
 
29
- const instructions = new TextRenderable(renderer, {
30
- id: "instructions",
31
- content: "\n[Enter] Resume [e] Edit session [d] Delete session [Esc] Back to home",
32
- fg: "#64748b",
38
+ const select = new SelectRenderable(renderer, {
39
+ id: "exit-select",
40
+ width: 40,
41
+ height: 4,
42
+ options: [
43
+ { name: "Resume session", description: "", value: "resume" },
44
+ { name: "Edit session", description: "", value: "edit" },
45
+ { name: "Delete session", description: "", value: "delete" },
46
+ { name: "Back to home", description: "", value: "home" },
47
+ ],
48
+ showDescription: false,
49
+ backgroundColor: "transparent",
50
+ focusedBackgroundColor: "transparent",
51
+ selectedBackgroundColor: "#334155",
52
+ textColor: "#e2e8f0",
53
+ selectedTextColor: "#38bdf8",
33
54
  marginTop: 1,
34
55
  });
35
- content.add(instructions);
56
+ content.add(select);
36
57
 
37
- const handleKeypress = (key: KeyEvent) => {
38
- if (key.name === "return" || key.name === "enter") {
39
- cleanup();
40
- resolve("resume");
41
- return;
42
- }
43
-
44
- if (key.name === "e") {
45
- cleanup();
46
- resolve("edit");
47
- return;
48
- }
58
+ select.focus();
49
59
 
50
- if (key.name === "d") {
51
- cleanup();
52
- resolve("delete");
53
- return;
54
- }
60
+ const handleSelect = (_index: number, option: { value: string }) => {
61
+ cleanup();
62
+ resolve(option.value as ExitChoice);
63
+ };
55
64
 
56
- if (key.name === "escape") {
65
+ const handleKeypress = (key: KeyEvent) => {
66
+ if (isEscape(key)) {
57
67
  cleanup();
58
68
  resolve("home");
59
69
  }
60
70
  };
61
71
 
62
72
  const cleanup = () => {
73
+ select.off(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
63
74
  renderer.keyInput.off("keypress", handleKeypress);
75
+ select.blur();
76
+ hideFooter(renderer);
64
77
  clearLayout(renderer);
65
78
  };
66
79
 
80
+ showFooter(renderer, content, {
81
+ navigate: true,
82
+ select: true,
83
+ back: true,
84
+ });
85
+
86
+ select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
67
87
  renderer.keyInput.on("keypress", handleKeypress);
68
88
  });
69
89
  }
@@ -109,47 +129,59 @@ export function showExitPromptWithChanges(
109
129
  id: "question",
110
130
  content: "What would you like to do?",
111
131
  fg: "#e2e8f0",
132
+ marginBottom: 1,
112
133
  });
113
134
  content.add(question);
114
135
 
115
- const instructions = new TextRenderable(renderer, {
116
- id: "instructions",
117
- content: "\n[Enter] Resume [e] Edit session [d] Delete session [Esc] Back to home",
118
- fg: "#64748b",
136
+ const select = new SelectRenderable(renderer, {
137
+ id: "exit-select",
138
+ width: 40,
139
+ height: 4,
140
+ options: [
141
+ { name: "Resume session", description: "", value: "resume" },
142
+ { name: "Edit session", description: "", value: "edit" },
143
+ { name: "Delete session", description: "", value: "delete" },
144
+ { name: "Back to home", description: "", value: "home" },
145
+ ],
146
+ showDescription: false,
147
+ backgroundColor: "transparent",
148
+ focusedBackgroundColor: "transparent",
149
+ selectedBackgroundColor: "#334155",
150
+ textColor: "#e2e8f0",
151
+ selectedTextColor: "#38bdf8",
119
152
  marginTop: 1,
120
153
  });
121
- content.add(instructions);
154
+ content.add(select);
122
155
 
123
- const handleKeypress = (key: KeyEvent) => {
124
- if (key.name === "return" || key.name === "enter") {
125
- cleanup();
126
- resolve("resume");
127
- return;
128
- }
129
-
130
- if (key.name === "e") {
131
- cleanup();
132
- resolve("edit");
133
- return;
134
- }
156
+ select.focus();
135
157
 
136
- if (key.name === "d") {
137
- cleanup();
138
- resolve("delete");
139
- return;
140
- }
158
+ const handleSelect = (_index: number, option: { value: string }) => {
159
+ cleanup();
160
+ resolve(option.value as ExitChoice);
161
+ };
141
162
 
142
- if (key.name === "escape") {
163
+ const handleKeypress = (key: KeyEvent) => {
164
+ if (isEscape(key)) {
143
165
  cleanup();
144
166
  resolve("home");
145
167
  }
146
168
  };
147
169
 
148
170
  const cleanup = () => {
171
+ select.off(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
149
172
  renderer.keyInput.off("keypress", handleKeypress);
173
+ select.blur();
174
+ hideFooter(renderer);
150
175
  clearLayout(renderer);
151
176
  };
152
177
 
178
+ showFooter(renderer, content, {
179
+ navigate: true,
180
+ select: true,
181
+ back: true,
182
+ });
183
+
184
+ select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
153
185
  renderer.keyInput.on("keypress", handleKeypress);
154
186
  });
155
187
  }
package/src/ui/list.ts CHANGED
@@ -8,6 +8,8 @@ import {
8
8
  import { createBaseLayout, clearLayout } from "./renderer";
9
9
  import { buildSessionOptions } from "./session-options";
10
10
  import type { Session } from "../types";
11
+ import { showFooter, hideFooter } from "./common/footer";
12
+ import { isEscape, isArrowUp, isArrowDown } from "./common/keyboard";
11
13
 
12
14
  export type ListAction =
13
15
  | { type: "resume"; session: Session }
@@ -19,7 +21,7 @@ export function showSessionList(renderer: CliRenderer, sessions: Session[]): Pro
19
21
  return new Promise((resolve) => {
20
22
  clearLayout(renderer);
21
23
 
22
- const { content } = createBaseLayout(renderer, "Sessions");
24
+ const { content } = createBaseLayout(renderer, `Sessions (${sessions.length})`);
23
25
 
24
26
  if (sessions.length === 0) {
25
27
  const emptyText = new TextRenderable(renderer, {
@@ -30,13 +32,21 @@ export function showSessionList(renderer: CliRenderer, sessions: Session[]): Pro
30
32
  content.add(emptyText);
31
33
 
32
34
  const handleKeypress = (key: KeyEvent) => {
33
- if (key.name === "escape") {
35
+ if (key.name === "q" || isEscape(key)) {
34
36
  renderer.keyInput.off("keypress", handleKeypress);
37
+ hideFooter(renderer);
35
38
  clearLayout(renderer);
36
39
  resolve({ type: "quit" });
37
40
  }
38
41
  };
39
42
 
43
+ showFooter(renderer, content, {
44
+ navigate: false,
45
+ select: false,
46
+ back: false,
47
+ quit: true,
48
+ });
49
+
40
50
  renderer.keyInput.on("keypress", handleKeypress);
41
51
  return;
42
52
  }
@@ -60,15 +70,6 @@ export function showSessionList(renderer: CliRenderer, sessions: Session[]): Pro
60
70
  });
61
71
  content.add(select);
62
72
 
63
- // Instructions
64
- const instructions = new TextRenderable(renderer, {
65
- id: "instructions",
66
- content: "\n[Enter] Resume [d] Delete [a] Nuke All [Esc] Quit",
67
- fg: "#64748b",
68
- marginTop: 1,
69
- });
70
- content.add(instructions);
71
-
72
73
  select.focus();
73
74
 
74
75
  let selectedIndex = 0;
@@ -80,9 +81,9 @@ export function showSessionList(renderer: CliRenderer, sessions: Session[]): Pro
80
81
 
81
82
  const handleKeypress = (key: KeyEvent) => {
82
83
  // Track selection for delete
83
- if (key.name === "up" || key.name === "k") {
84
+ if (isArrowUp(key)) {
84
85
  selectedIndex = Math.max(0, selectedIndex - 1);
85
- } else if (key.name === "down" || key.name === "j") {
86
+ } else if (isArrowDown(key)) {
86
87
  selectedIndex = Math.min(sessions.length - 1, selectedIndex + 1);
87
88
  } else if (key.name === "d") {
88
89
  const session = sessions[selectedIndex];
@@ -93,7 +94,7 @@ export function showSessionList(renderer: CliRenderer, sessions: Session[]): Pro
93
94
  } else if (key.name === "a") {
94
95
  cleanup();
95
96
  resolve({ type: "nuke-all" });
96
- } else if (key.name === "escape") {
97
+ } else if (key.name === "q" || isEscape(key)) {
97
98
  cleanup();
98
99
  resolve({ type: "quit" });
99
100
  }
@@ -103,9 +104,17 @@ export function showSessionList(renderer: CliRenderer, sessions: Session[]): Pro
103
104
  select.off(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
104
105
  renderer.keyInput.off("keypress", handleKeypress);
105
106
  select.blur();
107
+ hideFooter(renderer);
106
108
  clearLayout(renderer);
107
109
  };
108
110
 
111
+ showFooter(renderer, content, {
112
+ navigate: true,
113
+ select: false,
114
+ back: false,
115
+ custom: ["Enter Resume", "d Delete", "a Nuke All", "q Quit"],
116
+ });
117
+
109
118
  select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
110
119
  renderer.keyInput.on("keypress", handleKeypress);
111
120
  });
@@ -8,6 +8,8 @@ import {
8
8
  import { createBaseLayout, clearLayout } from "./renderer";
9
9
  import { buildSessionOptions } from "./session-options";
10
10
  import type { Session } from "../types";
11
+ import { showFooter, hideFooter } from "./common/footer";
12
+ import { isEscape, isArrowUp, isArrowDown } from "./common/keyboard";
11
13
 
12
14
  export type MainMenuAction =
13
15
  | { type: "new-session" }
@@ -19,23 +21,13 @@ export function showMainMenu(renderer: CliRenderer, sessions: Session[]): Promis
19
21
  return new Promise((resolve) => {
20
22
  clearLayout(renderer);
21
23
 
22
- const { content } = createBaseLayout(renderer, "letmecook");
24
+ const { content } = createBaseLayout(renderer);
23
25
 
24
- // Welcome text
25
- const welcome = new TextRenderable(renderer, {
26
- id: "welcome",
27
- content: "Multi-repo workspace manager for AI coding sessions",
28
- fg: "#94a3b8",
29
- marginBottom: 2,
30
- });
31
- content.add(welcome);
32
-
33
- // Sessions section
26
+ // Sessions section with count
34
27
  const sessionsHeader = new TextRenderable(renderer, {
35
28
  id: "sessions-header",
36
- content: "Sessions",
29
+ content: `Sessions (${sessions.length})`,
37
30
  fg: "#e2e8f0",
38
- marginTop: 1,
39
31
  marginBottom: 1,
40
32
  });
41
33
  content.add(sessionsHeader);
@@ -71,36 +63,6 @@ export function showMainMenu(renderer: CliRenderer, sessions: Session[]): Promis
71
63
  content.add(emptyText);
72
64
  }
73
65
 
74
- // Actions section
75
- const actionsHeader = new TextRenderable(renderer, {
76
- id: "actions-header",
77
- content: "Actions",
78
- fg: "#e2e8f0",
79
- marginTop: 1,
80
- marginBottom: 1,
81
- });
82
- content.add(actionsHeader);
83
-
84
- const actionsText = new TextRenderable(renderer, {
85
- id: "actions",
86
- content:
87
- sessions.length > 0
88
- ? "[n] New session\n[d] Delete session\n[Esc] Quit"
89
- : "[n] New session\n[Esc] Quit",
90
- fg: "#94a3b8",
91
- });
92
- content.add(actionsText);
93
-
94
- if (sessions.length > 0) {
95
- const instructions = new TextRenderable(renderer, {
96
- id: "instructions",
97
- content: "\n[Enter] Resume session",
98
- fg: "#64748b",
99
- marginTop: 1,
100
- });
101
- content.add(instructions);
102
- }
103
-
104
66
  if (select) {
105
67
  select.focus();
106
68
  }
@@ -113,9 +75,9 @@ export function showMainMenu(renderer: CliRenderer, sessions: Session[]): Promis
113
75
  };
114
76
 
115
77
  const handleKeypress = (key: KeyEvent) => {
116
- if (sessions.length > 0 && (key.name === "up" || key.name === "k")) {
78
+ if (sessions.length > 0 && isArrowUp(key)) {
117
79
  selectedIndex = Math.max(0, selectedIndex - 1);
118
- } else if (sessions.length > 0 && (key.name === "down" || key.name === "j")) {
80
+ } else if (sessions.length > 0 && isArrowDown(key)) {
119
81
  selectedIndex = Math.min(sessions.length - 1, selectedIndex + 1);
120
82
  } else if (key.name === "d" && sessions.length > 0) {
121
83
  const session = sessions[selectedIndex];
@@ -132,7 +94,7 @@ export function showMainMenu(renderer: CliRenderer, sessions: Session[]): Promis
132
94
  return;
133
95
  }
134
96
 
135
- if (key.name === "escape") {
97
+ if (key.name === "q" || isEscape(key)) {
136
98
  cleanup();
137
99
  resolve({ type: "quit" });
138
100
  }
@@ -144,9 +106,26 @@ export function showMainMenu(renderer: CliRenderer, sessions: Session[]): Promis
144
106
  select.blur();
145
107
  }
146
108
  renderer.keyInput.off("keypress", handleKeypress);
109
+ hideFooter(renderer);
147
110
  clearLayout(renderer);
148
111
  };
149
112
 
113
+ // Show footer with context-aware actions
114
+ const footerActions: string[] = [];
115
+ if (sessions.length > 0) {
116
+ footerActions.push("Enter Open", "n New", "d Delete");
117
+ } else {
118
+ footerActions.push("n New");
119
+ }
120
+ footerActions.push("q Quit");
121
+
122
+ showFooter(renderer, content, {
123
+ navigate: sessions.length > 0,
124
+ select: false,
125
+ back: false,
126
+ custom: footerActions,
127
+ });
128
+
150
129
  if (select) {
151
130
  select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
152
131
  }
@@ -7,6 +7,8 @@ import {
7
7
  } from "@opentui/core";
8
8
  import { createBaseLayout, clearLayout } from "./renderer";
9
9
  import type { RepoSpec } from "../types";
10
+ import { showFooter, hideFooter } from "./common/footer";
11
+ import { isEscape } from "./common/keyboard";
10
12
 
11
13
  export type RecloneChoice = "reclone" | "skip";
12
14
 
@@ -58,14 +60,6 @@ export function showReclonePrompt(renderer: CliRenderer, repo: RepoSpec): Promis
58
60
  });
59
61
  content.add(select);
60
62
 
61
- const instructions = new TextRenderable(renderer, {
62
- id: "instructions",
63
- content: "\n[Enter] Select [Esc] Skip",
64
- fg: "#64748b",
65
- marginTop: 1,
66
- });
67
- content.add(instructions);
68
-
69
63
  select.focus();
70
64
 
71
65
  const handleSelect = (_index: number, option: { value: string }) => {
@@ -74,7 +68,7 @@ export function showReclonePrompt(renderer: CliRenderer, repo: RepoSpec): Promis
74
68
  };
75
69
 
76
70
  const handleKeypress = (key: KeyEvent) => {
77
- if (key.name === "escape") {
71
+ if (isEscape(key)) {
78
72
  cleanup();
79
73
  resolve("skip");
80
74
  }
@@ -84,9 +78,17 @@ export function showReclonePrompt(renderer: CliRenderer, repo: RepoSpec): Promis
84
78
  select.off(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
85
79
  renderer.keyInput.off("keypress", handleKeypress);
86
80
  select.blur();
81
+ hideFooter(renderer);
87
82
  clearLayout(renderer);
88
83
  };
89
84
 
85
+ showFooter(renderer, content, {
86
+ navigate: true,
87
+ select: true,
88
+ back: true,
89
+ custom: ["Esc Skip"],
90
+ });
91
+
90
92
  select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
91
93
  renderer.keyInput.on("keypress", handleKeypress);
92
94
  });
@@ -101,8 +101,20 @@ export function createBaseLayout(r: CliRenderer, subtitle?: string): LayoutEleme
101
101
  }
102
102
 
103
103
  export function clearLayout(r: CliRenderer): void {
104
- // Remove known elements
105
- r.root.remove("main-container");
106
- r.root.remove("title");
107
- r.root.remove("content");
104
+ // Remove known elements (ignore if they don't exist)
105
+ try {
106
+ r.root.remove("main-container");
107
+ } catch {
108
+ // Element doesn't exist
109
+ }
110
+ try {
111
+ r.root.remove("title");
112
+ } catch {
113
+ // Element doesn't exist
114
+ }
115
+ try {
116
+ r.root.remove("content");
117
+ } catch {
118
+ // Element doesn't exist
119
+ }
108
120
  }
@@ -1,7 +1,15 @@
1
- import { type CliRenderer, TextRenderable, type KeyEvent } from "@opentui/core";
1
+ import {
2
+ type CliRenderer,
3
+ TextRenderable,
4
+ SelectRenderable,
5
+ SelectRenderableEvents,
6
+ type KeyEvent,
7
+ } from "@opentui/core";
2
8
  import { createBaseLayout, clearLayout } from "./renderer";
3
9
  import type { Session } from "../types";
4
10
  import { formatRepoList } from "./common/repo-formatter";
11
+ import { showFooter, hideFooter } from "./common/footer";
12
+ import { isEscape } from "./common/keyboard";
5
13
 
6
14
  export type SessionDetailsAction = "resume" | "edit" | "add-repos" | "back";
7
15
 
@@ -41,37 +49,54 @@ export function showSessionDetails(
41
49
  });
42
50
  content.add(reposInfo);
43
51
 
44
- const instructions = new TextRenderable(renderer, {
45
- id: "instructions",
46
- content: "\n[Enter] Resume [e] Edit settings [Esc] Back",
47
- fg: "#64748b",
52
+ const select = new SelectRenderable(renderer, {
53
+ id: "session-details-select",
54
+ width: 40,
55
+ height: 3,
56
+ options: [
57
+ { name: "Resume session", description: "", value: "resume" },
58
+ { name: "Edit settings", description: "", value: "edit" },
59
+ { name: "Back", description: "", value: "back" },
60
+ ],
61
+ showDescription: false,
62
+ backgroundColor: "transparent",
63
+ focusedBackgroundColor: "transparent",
64
+ selectedBackgroundColor: "#334155",
65
+ textColor: "#e2e8f0",
66
+ selectedTextColor: "#38bdf8",
48
67
  marginTop: 1,
49
68
  });
50
- content.add(instructions);
69
+ content.add(select);
51
70
 
52
- const handleKeypress = (key: KeyEvent) => {
53
- if (key.name === "return" || key.name === "enter") {
54
- cleanup();
55
- resolve("resume");
56
- return;
57
- }
71
+ select.focus();
58
72
 
59
- if (key.name === "e") {
60
- cleanup();
61
- resolve("edit");
62
- return;
63
- }
73
+ const handleSelect = (_index: number, option: { value: string }) => {
74
+ cleanup();
75
+ resolve(option.value as SessionDetailsAction);
76
+ };
64
77
 
65
- if (key.name === "escape") {
78
+ const handleKeypress = (key: KeyEvent) => {
79
+ if (isEscape(key)) {
66
80
  cleanup();
67
81
  resolve("back");
68
82
  }
69
83
  };
70
84
 
71
85
  const cleanup = () => {
86
+ select.off(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
72
87
  renderer.keyInput.off("keypress", handleKeypress);
88
+ select.blur();
89
+ hideFooter(renderer);
73
90
  clearLayout(renderer);
74
91
  };
92
+
93
+ showFooter(renderer, content, {
94
+ navigate: true,
95
+ select: true,
96
+ back: true,
97
+ });
98
+
99
+ select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
75
100
  renderer.keyInput.on("keypress", handleKeypress);
76
101
  });
77
102
  }