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/package.json +1 -1
- package/src/flows/add-repos.ts +52 -16
- package/src/flows/edit-session.ts +104 -46
- package/src/flows/new-session.ts +81 -76
- package/src/ui/add-repos.ts +184 -180
- package/src/ui/common/command-runner.ts +249 -0
- package/src/ui/common/footer.ts +105 -0
- package/src/ui/common/keyboard.ts +95 -0
- package/src/ui/confirm-delete.ts +10 -9
- package/src/ui/conflict.ts +10 -1
- package/src/ui/exit.ts +79 -47
- package/src/ui/list.ts +23 -14
- package/src/ui/main-menu.ts +25 -46
- package/src/ui/reclone-prompt.ts +11 -9
- package/src/ui/renderer.ts +16 -4
- package/src/ui/session-details.ts +43 -18
- package/src/ui/session-settings.ts +91 -71
- package/src/ui/skills.ts +44 -100
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { type CliRenderer, TextRenderable } from "@opentui/core";
|
|
2
|
+
import { createBaseLayout, clearLayout } from "../renderer";
|
|
3
|
+
import { readProcessOutputWithBuffer } from "../../utils/stream";
|
|
4
|
+
|
|
5
|
+
export interface CommandTask {
|
|
6
|
+
label: string; // "Cloning microsoft/playwright"
|
|
7
|
+
command: string[]; // ["git", "clone", "https://..."]
|
|
8
|
+
cwd?: string; // Working directory
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CommandRunnerOptions {
|
|
12
|
+
title: string; // "Setting up session"
|
|
13
|
+
tasks: CommandTask[]; // Array of commands to run
|
|
14
|
+
showOutput?: boolean; // Show last N lines (default: true)
|
|
15
|
+
outputLines?: number; // How many lines to show (default: 5)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface CommandResult {
|
|
19
|
+
task: CommandTask;
|
|
20
|
+
success: boolean;
|
|
21
|
+
exitCode: number;
|
|
22
|
+
output: string[];
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let taskTexts: TextRenderable[] = [];
|
|
27
|
+
let currentCommandText: TextRenderable | null = null;
|
|
28
|
+
let outputText: TextRenderable | null = null;
|
|
29
|
+
let tasksLabel: TextRenderable | null = null;
|
|
30
|
+
|
|
31
|
+
function getStatusIcon(status: "pending" | "running" | "done" | "error"): {
|
|
32
|
+
icon: string;
|
|
33
|
+
color: string;
|
|
34
|
+
} {
|
|
35
|
+
switch (status) {
|
|
36
|
+
case "done":
|
|
37
|
+
return { icon: "[✓]", color: "#22c55e" };
|
|
38
|
+
case "running":
|
|
39
|
+
return { icon: "[~]", color: "#fbbf24" };
|
|
40
|
+
case "error":
|
|
41
|
+
return { icon: "[✗]", color: "#ef4444" };
|
|
42
|
+
case "pending":
|
|
43
|
+
default:
|
|
44
|
+
return { icon: "[ ]", color: "#94a3b8" };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function showCommandRunner(
|
|
49
|
+
renderer: CliRenderer,
|
|
50
|
+
options: CommandRunnerOptions,
|
|
51
|
+
): {
|
|
52
|
+
taskStatuses: Array<{ task: CommandTask; status: "pending" | "running" | "done" | "error" }>;
|
|
53
|
+
} {
|
|
54
|
+
clearLayout(renderer);
|
|
55
|
+
taskTexts = [];
|
|
56
|
+
|
|
57
|
+
const { title, tasks, showOutput = true } = options;
|
|
58
|
+
|
|
59
|
+
const { content } = createBaseLayout(renderer, title);
|
|
60
|
+
|
|
61
|
+
tasksLabel = new TextRenderable(renderer, {
|
|
62
|
+
id: "tasks-label",
|
|
63
|
+
content: "Tasks",
|
|
64
|
+
fg: "#e2e8f0",
|
|
65
|
+
marginTop: 1,
|
|
66
|
+
marginBottom: 1,
|
|
67
|
+
});
|
|
68
|
+
content.add(tasksLabel);
|
|
69
|
+
|
|
70
|
+
const taskStatuses = tasks.map((task) => ({ task, status: "pending" as const }));
|
|
71
|
+
|
|
72
|
+
tasks.forEach((task, i) => {
|
|
73
|
+
const statusIcon = getStatusIcon("pending");
|
|
74
|
+
const taskText = new TextRenderable(renderer, {
|
|
75
|
+
id: `task-${i}`,
|
|
76
|
+
content: `${statusIcon.icon} ${task.label}`,
|
|
77
|
+
fg: statusIcon.color,
|
|
78
|
+
});
|
|
79
|
+
content.add(taskText);
|
|
80
|
+
taskTexts.push(taskText);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (showOutput) {
|
|
84
|
+
currentCommandText = new TextRenderable(renderer, {
|
|
85
|
+
id: "current-command",
|
|
86
|
+
content: "",
|
|
87
|
+
fg: "#64748b",
|
|
88
|
+
marginTop: 2,
|
|
89
|
+
});
|
|
90
|
+
content.add(currentCommandText);
|
|
91
|
+
|
|
92
|
+
const separator = new TextRenderable(renderer, {
|
|
93
|
+
id: "output-separator",
|
|
94
|
+
content: "──────────────────────────────────────",
|
|
95
|
+
fg: "#475569",
|
|
96
|
+
marginTop: 0,
|
|
97
|
+
});
|
|
98
|
+
content.add(separator);
|
|
99
|
+
|
|
100
|
+
outputText = new TextRenderable(renderer, {
|
|
101
|
+
id: "command-output",
|
|
102
|
+
content: "",
|
|
103
|
+
fg: "#64748b",
|
|
104
|
+
marginTop: 0,
|
|
105
|
+
});
|
|
106
|
+
content.add(outputText);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { taskStatuses };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function updateCommandRunner(
|
|
113
|
+
renderer: CliRenderer,
|
|
114
|
+
taskStatuses: Array<{ task: CommandTask; status: "pending" | "running" | "done" | "error" }>,
|
|
115
|
+
currentTaskIndex?: number,
|
|
116
|
+
outputLines?: string[],
|
|
117
|
+
): void {
|
|
118
|
+
taskStatuses.forEach((item, i) => {
|
|
119
|
+
const text = taskTexts[i];
|
|
120
|
+
if (text) {
|
|
121
|
+
const statusIcon = getStatusIcon(item.status);
|
|
122
|
+
text.content = `${statusIcon.icon} ${item.task.label}`;
|
|
123
|
+
text.fg = statusIcon.color;
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (currentCommandText && currentTaskIndex !== undefined && currentTaskIndex >= 0) {
|
|
128
|
+
const currentTask = taskStatuses[currentTaskIndex];
|
|
129
|
+
if (currentTask) {
|
|
130
|
+
const commandStr = currentTask.task.command.join(" ");
|
|
131
|
+
currentCommandText.content = `Running: ${commandStr}`;
|
|
132
|
+
currentCommandText.fg = "#38bdf8";
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (outputText && outputLines && outputLines.length > 0) {
|
|
137
|
+
outputText.content = outputLines.map((line) => ` ${line}`).join("\n");
|
|
138
|
+
} else if (outputText) {
|
|
139
|
+
outputText.content = "";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
renderer.requestRender();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function hideCommandRunner(renderer: CliRenderer): void {
|
|
146
|
+
clearLayout(renderer);
|
|
147
|
+
taskTexts = [];
|
|
148
|
+
currentCommandText = null;
|
|
149
|
+
outputText = null;
|
|
150
|
+
tasksLabel = null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function runCommands(
|
|
154
|
+
renderer: CliRenderer,
|
|
155
|
+
options: CommandRunnerOptions,
|
|
156
|
+
): Promise<CommandResult[]> {
|
|
157
|
+
const { tasks, showOutput = true, outputLines = 5 } = options;
|
|
158
|
+
|
|
159
|
+
const { taskStatuses } = showCommandRunner(renderer, options);
|
|
160
|
+
const results: CommandResult[] = [];
|
|
161
|
+
|
|
162
|
+
for (let i = 0; i < tasks.length; i++) {
|
|
163
|
+
const task = tasks[i];
|
|
164
|
+
const taskState = taskStatuses[i];
|
|
165
|
+
|
|
166
|
+
if (!task || !taskState) continue;
|
|
167
|
+
|
|
168
|
+
// Update status to running
|
|
169
|
+
taskState.status = "running";
|
|
170
|
+
updateCommandRunner(renderer, taskStatuses, i, []);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const proc = Bun.spawn(task.command, {
|
|
174
|
+
cwd: task.cwd,
|
|
175
|
+
stdout: "pipe",
|
|
176
|
+
stderr: "pipe",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
let outputBuffer: string[] = [];
|
|
180
|
+
|
|
181
|
+
const { success, output, fullOutput } = await readProcessOutputWithBuffer(proc, {
|
|
182
|
+
maxBufferLines: outputLines,
|
|
183
|
+
onBufferUpdate: (buffer) => {
|
|
184
|
+
outputBuffer = buffer;
|
|
185
|
+
if (showOutput) {
|
|
186
|
+
updateCommandRunner(renderer, taskStatuses, i, buffer);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const exitCode = await proc.exited;
|
|
192
|
+
|
|
193
|
+
if (success && exitCode === 0) {
|
|
194
|
+
taskState.status = "done";
|
|
195
|
+
results.push({
|
|
196
|
+
task,
|
|
197
|
+
success: true,
|
|
198
|
+
exitCode: 0,
|
|
199
|
+
output: outputBuffer.length > 0 ? outputBuffer : output,
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
taskState.status = "error";
|
|
203
|
+
const errorMsg = fullOutput.trim() || `Command exited with code ${exitCode}`;
|
|
204
|
+
results.push({
|
|
205
|
+
task,
|
|
206
|
+
success: false,
|
|
207
|
+
exitCode,
|
|
208
|
+
output: outputBuffer.length > 0 ? outputBuffer : output,
|
|
209
|
+
error: errorMsg,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Final update with last output
|
|
214
|
+
if (showOutput) {
|
|
215
|
+
updateCommandRunner(
|
|
216
|
+
renderer,
|
|
217
|
+
taskStatuses,
|
|
218
|
+
i,
|
|
219
|
+
outputBuffer.length > 0 ? outputBuffer : output,
|
|
220
|
+
);
|
|
221
|
+
} else {
|
|
222
|
+
updateCommandRunner(renderer, taskStatuses, i);
|
|
223
|
+
}
|
|
224
|
+
} catch (error) {
|
|
225
|
+
taskState.status = "error";
|
|
226
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
227
|
+
results.push({
|
|
228
|
+
task,
|
|
229
|
+
success: false,
|
|
230
|
+
exitCode: 1,
|
|
231
|
+
output: [],
|
|
232
|
+
error: errorMsg,
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
if (showOutput) {
|
|
236
|
+
updateCommandRunner(renderer, taskStatuses, i, [errorMsg]);
|
|
237
|
+
} else {
|
|
238
|
+
updateCommandRunner(renderer, taskStatuses, i);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Clear current command indicator when done
|
|
244
|
+
if (currentCommandText) {
|
|
245
|
+
currentCommandText.content = "";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return results;
|
|
249
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type CliRenderer, TextRenderable, type Renderable } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
export interface FooterActions {
|
|
4
|
+
navigate?: boolean; // Show ↑↓ Navigate
|
|
5
|
+
select?: boolean; // Show Enter Select
|
|
6
|
+
back?: boolean; // Show Esc Back
|
|
7
|
+
new?: boolean; // Show n New
|
|
8
|
+
delete?: boolean; // Show d Delete
|
|
9
|
+
quit?: boolean; // Show q Quit
|
|
10
|
+
custom?: string[]; // Custom action hints like ["Tab Switch", "s Save"]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let footerSeparator: TextRenderable | null = null;
|
|
14
|
+
let footerText: TextRenderable | null = null;
|
|
15
|
+
let footerParent: Renderable | null = null;
|
|
16
|
+
|
|
17
|
+
export function showFooter(
|
|
18
|
+
renderer: CliRenderer,
|
|
19
|
+
parent: Renderable,
|
|
20
|
+
actions: FooterActions = {},
|
|
21
|
+
): void {
|
|
22
|
+
const {
|
|
23
|
+
navigate = true,
|
|
24
|
+
select = true,
|
|
25
|
+
back = true,
|
|
26
|
+
new: showNew = false,
|
|
27
|
+
delete: showDelete = false,
|
|
28
|
+
quit = false,
|
|
29
|
+
custom = [],
|
|
30
|
+
} = actions;
|
|
31
|
+
|
|
32
|
+
const parts: string[] = [];
|
|
33
|
+
|
|
34
|
+
if (navigate) {
|
|
35
|
+
parts.push("↑↓ Navigate");
|
|
36
|
+
}
|
|
37
|
+
if (select) {
|
|
38
|
+
parts.push("Enter Select");
|
|
39
|
+
}
|
|
40
|
+
if (back) {
|
|
41
|
+
parts.push("Esc Back");
|
|
42
|
+
}
|
|
43
|
+
if (showNew) {
|
|
44
|
+
parts.push("n New");
|
|
45
|
+
}
|
|
46
|
+
if (showDelete) {
|
|
47
|
+
parts.push("d Delete");
|
|
48
|
+
}
|
|
49
|
+
if (quit) {
|
|
50
|
+
parts.push("q Quit");
|
|
51
|
+
}
|
|
52
|
+
if (custom.length > 0) {
|
|
53
|
+
parts.push(...custom);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const footerContent = parts.join(" ");
|
|
57
|
+
|
|
58
|
+
// Hide any existing footer first
|
|
59
|
+
hideFooter(renderer);
|
|
60
|
+
|
|
61
|
+
// Create separator line
|
|
62
|
+
const separator = new TextRenderable(renderer, {
|
|
63
|
+
id: "footer-separator",
|
|
64
|
+
content: "─".repeat(66), // Approximate width for separator line
|
|
65
|
+
fg: "#475569",
|
|
66
|
+
marginTop: 1,
|
|
67
|
+
});
|
|
68
|
+
parent.add(separator);
|
|
69
|
+
|
|
70
|
+
// Create footer text
|
|
71
|
+
footerText = new TextRenderable(renderer, {
|
|
72
|
+
id: "footer-text",
|
|
73
|
+
content: footerContent,
|
|
74
|
+
fg: "#64748b",
|
|
75
|
+
});
|
|
76
|
+
parent.add(footerText);
|
|
77
|
+
|
|
78
|
+
footerSeparator = separator;
|
|
79
|
+
footerParent = parent;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function updateFooter(
|
|
83
|
+
renderer: CliRenderer,
|
|
84
|
+
parent: Renderable,
|
|
85
|
+
actions: FooterActions,
|
|
86
|
+
): void {
|
|
87
|
+
hideFooter(renderer);
|
|
88
|
+
showFooter(renderer, parent, actions);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function hideFooter(_renderer: CliRenderer): void {
|
|
92
|
+
if (footerSeparator) {
|
|
93
|
+
if (footerParent) {
|
|
94
|
+
footerParent.remove("footer-separator");
|
|
95
|
+
}
|
|
96
|
+
footerSeparator = null;
|
|
97
|
+
}
|
|
98
|
+
if (footerText) {
|
|
99
|
+
if (footerParent) {
|
|
100
|
+
footerParent.remove("footer-text");
|
|
101
|
+
}
|
|
102
|
+
footerText = null;
|
|
103
|
+
}
|
|
104
|
+
footerParent = null;
|
|
105
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { KeyEvent } from "@opentui/core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Unified keyboard mapping constants for consistent navigation across the TUI.
|
|
5
|
+
*
|
|
6
|
+
* Design principles:
|
|
7
|
+
* - Arrow keys everywhere for navigation
|
|
8
|
+
* - Enter always confirms/selects
|
|
9
|
+
* - Esc always goes back/cancels
|
|
10
|
+
* - Tab moves between fields/sections
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const KEYBOARD = {
|
|
14
|
+
// Navigation
|
|
15
|
+
UP: "up",
|
|
16
|
+
DOWN: "down",
|
|
17
|
+
LEFT: "left",
|
|
18
|
+
RIGHT: "right",
|
|
19
|
+
TAB: "tab",
|
|
20
|
+
|
|
21
|
+
// Actions
|
|
22
|
+
ENTER: "enter",
|
|
23
|
+
RETURN: "return",
|
|
24
|
+
ESCAPE: "escape",
|
|
25
|
+
|
|
26
|
+
// Global shortcuts (only on list screens)
|
|
27
|
+
NEW: "n",
|
|
28
|
+
DELETE: "d",
|
|
29
|
+
QUIT: "q",
|
|
30
|
+
|
|
31
|
+
// Text input shortcuts
|
|
32
|
+
BACKSPACE: "backspace",
|
|
33
|
+
CTRL_D: "d", // Ctrl+D (handled via ctrl modifier)
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Check if a key event matches a specific key name
|
|
38
|
+
*/
|
|
39
|
+
export function isKey(key: KeyEvent, name: string): boolean {
|
|
40
|
+
return key.name === name;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a key event matches Enter/Return
|
|
45
|
+
*/
|
|
46
|
+
export function isEnter(key: KeyEvent): boolean {
|
|
47
|
+
return key.name === KEYBOARD.ENTER || key.name === KEYBOARD.RETURN;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a key event matches Escape
|
|
52
|
+
*/
|
|
53
|
+
export function isEscape(key: KeyEvent): boolean {
|
|
54
|
+
return key.name === KEYBOARD.ESCAPE;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Check if a key event matches Tab
|
|
59
|
+
*/
|
|
60
|
+
export function isTab(key: KeyEvent): boolean {
|
|
61
|
+
return key.name === KEYBOARD.TAB;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if a key event matches arrow up
|
|
66
|
+
*/
|
|
67
|
+
export function isArrowUp(key: KeyEvent): boolean {
|
|
68
|
+
return key.name === KEYBOARD.UP;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if a key event matches arrow down
|
|
73
|
+
*/
|
|
74
|
+
export function isArrowDown(key: KeyEvent): boolean {
|
|
75
|
+
return key.name === KEYBOARD.DOWN;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Check if a key event is Ctrl+D
|
|
80
|
+
*/
|
|
81
|
+
export function isCtrlD(key: KeyEvent): boolean {
|
|
82
|
+
return key.name === KEYBOARD.CTRL_D && key.ctrl === true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if a key event is a navigation key (arrows)
|
|
87
|
+
*/
|
|
88
|
+
export function isNavigation(key: KeyEvent): boolean {
|
|
89
|
+
return (
|
|
90
|
+
key.name === KEYBOARD.UP ||
|
|
91
|
+
key.name === KEYBOARD.DOWN ||
|
|
92
|
+
key.name === KEYBOARD.LEFT ||
|
|
93
|
+
key.name === KEYBOARD.RIGHT
|
|
94
|
+
);
|
|
95
|
+
}
|
package/src/ui/confirm-delete.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
} from "@opentui/core";
|
|
8
8
|
import { createBaseLayout, clearLayout } from "./renderer";
|
|
9
9
|
import type { Session } from "../types";
|
|
10
|
+
import { showFooter, hideFooter } from "./common/footer";
|
|
11
|
+
import { isEscape } from "./common/keyboard";
|
|
10
12
|
|
|
11
13
|
export type DeleteConfirmChoice = "confirm" | "cancel";
|
|
12
14
|
|
|
@@ -60,14 +62,6 @@ export function showDeleteConfirm(
|
|
|
60
62
|
});
|
|
61
63
|
content.add(select);
|
|
62
64
|
|
|
63
|
-
const instructions = new TextRenderable(renderer, {
|
|
64
|
-
id: "instructions",
|
|
65
|
-
content: "\n[Enter] Select [Esc] Cancel",
|
|
66
|
-
fg: "#64748b",
|
|
67
|
-
marginTop: 1,
|
|
68
|
-
});
|
|
69
|
-
content.add(instructions);
|
|
70
|
-
|
|
71
65
|
select.focus();
|
|
72
66
|
|
|
73
67
|
const handleSelect = (_index: number, option: { value: string }) => {
|
|
@@ -76,7 +70,7 @@ export function showDeleteConfirm(
|
|
|
76
70
|
};
|
|
77
71
|
|
|
78
72
|
const handleKeypress = (key: KeyEvent) => {
|
|
79
|
-
if (key
|
|
73
|
+
if (isEscape(key)) {
|
|
80
74
|
cleanup();
|
|
81
75
|
resolve("cancel");
|
|
82
76
|
}
|
|
@@ -86,9 +80,16 @@ export function showDeleteConfirm(
|
|
|
86
80
|
select.off(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
87
81
|
renderer.keyInput.off("keypress", handleKeypress);
|
|
88
82
|
select.blur();
|
|
83
|
+
hideFooter(renderer);
|
|
89
84
|
clearLayout(renderer);
|
|
90
85
|
};
|
|
91
86
|
|
|
87
|
+
showFooter(renderer, content, {
|
|
88
|
+
navigate: true,
|
|
89
|
+
select: true,
|
|
90
|
+
back: true,
|
|
91
|
+
});
|
|
92
|
+
|
|
92
93
|
select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
93
94
|
renderer.keyInput.on("keypress", handleKeypress);
|
|
94
95
|
});
|
package/src/ui/conflict.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
} from "@opentui/core";
|
|
8
8
|
import { createBaseLayout, clearLayout } from "./renderer";
|
|
9
9
|
import type { Session, ConflictChoice } from "../types";
|
|
10
|
+
import { showFooter, hideFooter } from "./common/footer";
|
|
11
|
+
import { isEscape } from "./common/keyboard";
|
|
10
12
|
|
|
11
13
|
function formatTimeAgo(date: string): string {
|
|
12
14
|
const now = new Date();
|
|
@@ -102,7 +104,7 @@ export function showConflictPrompt(
|
|
|
102
104
|
};
|
|
103
105
|
|
|
104
106
|
const handleKeypress = (key: KeyEvent) => {
|
|
105
|
-
if (key
|
|
107
|
+
if (isEscape(key)) {
|
|
106
108
|
cleanup();
|
|
107
109
|
resolve("cancel");
|
|
108
110
|
}
|
|
@@ -112,9 +114,16 @@ export function showConflictPrompt(
|
|
|
112
114
|
select.off(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
113
115
|
renderer.keyInput.off("keypress", handleKeypress);
|
|
114
116
|
select.blur();
|
|
117
|
+
hideFooter(renderer);
|
|
115
118
|
clearLayout(renderer);
|
|
116
119
|
};
|
|
117
120
|
|
|
121
|
+
showFooter(renderer, content, {
|
|
122
|
+
navigate: true,
|
|
123
|
+
select: true,
|
|
124
|
+
back: true,
|
|
125
|
+
});
|
|
126
|
+
|
|
118
127
|
select.on(SelectRenderableEvents.ITEM_SELECTED, handleSelect);
|
|
119
128
|
renderer.keyInput.on("keypress", handleKeypress);
|
|
120
129
|
});
|