nou-cli 0.5.3 → 0.5.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 +7 -4
- package/dist/src/config/index.js +2 -0
- package/dist/src/guide/index.js +100 -0
- package/dist/src/syntax/index.js +17 -17
- package/dist/src/ui/App.js +336 -42
- package/dist/src/ui/Chat.js +100 -19
- package/dist/src/ui/Editor.js +44 -7
- package/dist/src/ui/FileTree.js +2 -2
- package/dist/src/ui/GamePanel.js +1 -1
- package/dist/src/ui/Header.js +2 -2
- package/dist/src/ui/ProblemsView.js +2 -2
- package/dist/src/ui/Settings.js +2 -2
- package/dist/src/ui/Terminal.js +5 -2
- package/dist/src/ui/clipboard.js +19 -0
- package/dist/src/ui/commands.js +17 -0
- package/dist/src/ui/text.js +31 -3
- package/dist/src/ui/theme.js +2 -0
- package/package.json +1 -1
package/dist/src/ui/App.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
4
|
import path from "path";
|
|
5
5
|
import { Box, Text, useApp, useInput, useWindowSize } from "ink";
|
|
6
6
|
import { nouRulesOnly } from "../context/index.js";
|
|
@@ -30,14 +30,15 @@ import { C } from "./theme.js";
|
|
|
30
30
|
import { FileTree } from "./FileTree.js";
|
|
31
31
|
import { CodeView } from "./CodeView.js";
|
|
32
32
|
import { Editor } from "./Editor.js";
|
|
33
|
-
import { SLASH_COMMANDS } from "./commands.js";
|
|
33
|
+
import { SLASH_COMMANDS, runsWhileBusy } from "./commands.js";
|
|
34
34
|
import { WHEEL_STEP } from "./scroll.js";
|
|
35
|
-
import { Chat } from "./Chat.js";
|
|
36
|
-
import { copyToClipboard } from "./clipboard.js";
|
|
35
|
+
import { Chat, chatTabZones } from "./Chat.js";
|
|
36
|
+
import { copyToClipboard, openInBrowser } from "./clipboard.js";
|
|
37
37
|
import { Header } from "./Header.js";
|
|
38
38
|
import { Tabs, layoutTabs } from "./Tabs.js";
|
|
39
39
|
import { GAME_PANEL, GamePanel } from "./GamePanel.js";
|
|
40
40
|
import { Terminal } from "./Terminal.js";
|
|
41
|
+
import { showMePrompt, showStepsTool, stepsFromReply } from "../guide/index.js";
|
|
41
42
|
import { Shell } from "../terminal/index.js";
|
|
42
43
|
import { QuickOpen, listAllFiles, score } from "./QuickOpen.js";
|
|
43
44
|
import { Settings } from "./Settings.js";
|
|
@@ -124,6 +125,21 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
124
125
|
/** The terminal panel under the code (` or /terminal): its shell lives on while the panel is closed. */
|
|
125
126
|
const [termOpen, setTermOpen] = useState(false);
|
|
126
127
|
const [termVersion, setTermVersion] = useState(0);
|
|
128
|
+
/** Its height while its top border is being dragged (saved when you let go). */
|
|
129
|
+
const [termDrag, setTermDrag] = useState(null);
|
|
130
|
+
/** The Games panel's height while its top border is being dragged (saved when you let go). */
|
|
131
|
+
const [gameSizeDrag, setGameSizeDrag] = useState(null);
|
|
132
|
+
/** The tab strip, kept while nothing in it changes: a new one would redraw the whole code view on every scroll. */
|
|
133
|
+
const tabStripCache = useRef(null);
|
|
134
|
+
/**
|
|
135
|
+
* "Show me how" (/showme): the steps Nou found, and the one you're on. While Nou looks for them,
|
|
136
|
+
* `guideAsk` holds what you asked (and its edits are turned down: you're doing the typing).
|
|
137
|
+
*/
|
|
138
|
+
const [guide, setGuide] = useState(null);
|
|
139
|
+
const guideAsk = useRef(null);
|
|
140
|
+
/** Where the undo history stood when the steps were asked for: an engine that edits anyway is put back. */
|
|
141
|
+
const guideMark = useRef(0);
|
|
142
|
+
const guideFound = useRef(null);
|
|
127
143
|
const shellRef = useRef(null);
|
|
128
144
|
const beforeTerm = useRef("code");
|
|
129
145
|
const [follow, setFollow] = useState(settings.followMode);
|
|
@@ -287,34 +303,11 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
287
303
|
openAtRef.current(filePath, line ?? 1, false, to);
|
|
288
304
|
},
|
|
289
305
|
onRead: (filePath, line, to) => followStepRef.current({ file: filePath, ...(line ? { line } : {}), ...(to ? { to } : {}) }),
|
|
290
|
-
onChange: (change, status, turn) =>
|
|
291
|
-
if (status === "applied") {
|
|
292
|
-
timelineRef.current.record({ path: change.path, before: change.isNew ? null : change.before, after: change.after, source: "nou", turn });
|
|
293
|
-
}
|
|
294
|
-
const lines = status === "applied" ? changedLines(change.before, change.after) : NO_LINES;
|
|
295
|
-
setChanges((prev) => new Map(prev).set(change.path, { change, status, lines }));
|
|
296
|
-
setWorking((w) => {
|
|
297
|
-
if (status === "rejected")
|
|
298
|
-
return w.has(change.path) ? new Set([...w].filter((p) => p !== change.path)) : w;
|
|
299
|
-
return w.has(change.path) ? w : new Set(w).add(change.path);
|
|
300
|
-
});
|
|
301
|
-
// Diffs awaiting approval are shown unless you're in the middle of editing a file (the chat says what's waiting).
|
|
302
|
-
if ((status === "pending" && !draftActiveRef.current) || followRef.current) {
|
|
303
|
-
clearFollow(); // an edit is shown as it happens, ahead of reads still waiting
|
|
304
|
-
setOpenFile(change.path);
|
|
305
|
-
setContent(status === "applied" ? change.after : readText(change.path));
|
|
306
|
-
setDiffMode("off");
|
|
307
|
-
setReveal(change.path);
|
|
308
|
-
}
|
|
309
|
-
if (status === "applied") {
|
|
310
|
-
setTreeVersion((v) => v + 1);
|
|
311
|
-
setGit(gitStatus());
|
|
312
|
-
if (draft.current?.path === change.path) {
|
|
313
|
-
setEntries((prev) => [...prev, { kind: "info", text: `Heads up: you have unsaved edits in ${change.path}; saving them will overwrite Nou's change.` }]);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
},
|
|
306
|
+
onChange: (change, status, turn) => applyChange(change, status, turn, false),
|
|
317
307
|
approve: (request) => {
|
|
308
|
+
// Asked to show how, not to do it: edits are turned down (the model is told to hand back steps).
|
|
309
|
+
if (guideAsk.current && request.kind === "change")
|
|
310
|
+
return Promise.resolve(false);
|
|
318
311
|
const s = settingsRef.current;
|
|
319
312
|
if ((request.kind === "change" && s.autoApproveEdits) || (request.kind !== "change" && s.autoApproveCommands)) {
|
|
320
313
|
return Promise.resolve(true);
|
|
@@ -324,9 +317,154 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
324
317
|
checkChange: (change) => problemsIntroduced(diagnosticsRef.current, change),
|
|
325
318
|
};
|
|
326
319
|
}
|
|
320
|
+
/** A change an engine made or proposes; one from a tab in the background doesn't take over the code view. */
|
|
321
|
+
function applyChange(change, status, turn, background) {
|
|
322
|
+
if (status === "applied") {
|
|
323
|
+
timelineRef.current.record({ path: change.path, before: change.isNew ? null : change.before, after: change.after, source: "nou", turn });
|
|
324
|
+
}
|
|
325
|
+
const lines = status === "applied" ? changedLines(change.before, change.after) : NO_LINES;
|
|
326
|
+
setChanges((prev) => new Map(prev).set(change.path, { change, status, lines }));
|
|
327
|
+
setWorking((w) => {
|
|
328
|
+
if (status === "rejected")
|
|
329
|
+
return w.has(change.path) ? new Set([...w].filter((p) => p !== change.path)) : w;
|
|
330
|
+
return w.has(change.path) ? w : new Set(w).add(change.path);
|
|
331
|
+
});
|
|
332
|
+
// Diffs awaiting approval are shown unless you're in the middle of editing a file (the chat says what's waiting).
|
|
333
|
+
if (!background && ((status === "pending" && !draftActiveRef.current) || followRef.current)) {
|
|
334
|
+
clearFollow(); // an edit is shown as it happens, ahead of reads still waiting
|
|
335
|
+
setOpenFile(change.path);
|
|
336
|
+
setContent(status === "applied" ? change.after : readText(change.path));
|
|
337
|
+
setDiffMode("off");
|
|
338
|
+
setReveal(change.path);
|
|
339
|
+
}
|
|
340
|
+
if (status === "applied") {
|
|
341
|
+
setTreeVersion((v) => v + 1);
|
|
342
|
+
setGit(gitStatus());
|
|
343
|
+
if (draft.current?.path === change.path) {
|
|
344
|
+
setEntries((prev) => [...prev, { kind: "info", text: `Heads up: you have unsaved edits in ${change.path}; saving them will overwrite Nou's change.` }]);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Conversation tabs: several agents at once. The one on screen uses the state above; the others keep
|
|
349
|
+
// theirs here, and each tab's engine reports through hooks that send it to the right place.
|
|
350
|
+
const activeTab = useRef("1");
|
|
351
|
+
const [activeTabId, setActiveTabId] = useState("1");
|
|
352
|
+
const [chatTabs, setChatTabs] = useState(["1"]);
|
|
353
|
+
const tabStore = useRef(new Map());
|
|
354
|
+
const [, setTabVersion] = useState(0);
|
|
355
|
+
const tabHooks = useRef(new Map());
|
|
356
|
+
/** The tab strip's data, kept while it reads the same, so the chat doesn't redraw on every app render. */
|
|
357
|
+
const chatTabsCache = useRef(null);
|
|
358
|
+
/** The hooks for tab `id`'s engine: on screen, they're the chat's own; in the background, its stored state. */
|
|
359
|
+
function hooksFor(id) {
|
|
360
|
+
const cached = tabHooks.current.get(id);
|
|
361
|
+
if (cached)
|
|
362
|
+
return cached;
|
|
363
|
+
const live = () => hooksRef.current;
|
|
364
|
+
const shown = () => activeTab.current === id;
|
|
365
|
+
const stored = (fn) => {
|
|
366
|
+
const t = tabStore.current.get(id);
|
|
367
|
+
if (!t)
|
|
368
|
+
return;
|
|
369
|
+
fn(t);
|
|
370
|
+
setTabVersion((v) => v + 1);
|
|
371
|
+
};
|
|
372
|
+
const hooks = {
|
|
373
|
+
onEntry: (entry) => (shown() ? live().onEntry(entry) : stored((t) => (t.entries = [...t.entries, entry]))),
|
|
374
|
+
onStream: (text) => (shown() ? live().onStream(text) : void (tabStore.current.get(id) && (tabStore.current.get(id).streaming = text))),
|
|
375
|
+
onBusy: (label) => {
|
|
376
|
+
if (shown())
|
|
377
|
+
return live().onBusy(label);
|
|
378
|
+
stored((t) => {
|
|
379
|
+
t.busy = label;
|
|
380
|
+
if (!label)
|
|
381
|
+
saveTab(t); // it finished in the background: keep it for /resume
|
|
382
|
+
});
|
|
383
|
+
},
|
|
384
|
+
onOpen: (...args) => (shown() ? live().onOpen?.(...args) : undefined),
|
|
385
|
+
onRead: (...args) => (shown() ? live().onRead(...args) : undefined),
|
|
386
|
+
onChange: (change, status, turn) => applyChange(change, status, turn, !shown()),
|
|
387
|
+
approve: (request) => {
|
|
388
|
+
if (shown())
|
|
389
|
+
return live().approve(request);
|
|
390
|
+
const s = settingsRef.current;
|
|
391
|
+
if ((request.kind === "change" && s.autoApproveEdits) || (request.kind !== "change" && s.autoApproveCommands))
|
|
392
|
+
return Promise.resolve(true);
|
|
393
|
+
// It waits for you: its tab says so, and the question is there when you switch to it.
|
|
394
|
+
return new Promise((resolve) => stored((t) => (t.approval = { request, resolve })));
|
|
395
|
+
},
|
|
396
|
+
checkChange: (change) => live().checkChange?.(change) ?? Promise.resolve(null),
|
|
397
|
+
};
|
|
398
|
+
tabHooks.current.set(id, hooks);
|
|
399
|
+
return hooks;
|
|
400
|
+
}
|
|
401
|
+
/** Puts tab `id` on screen, keeping the one there now (and whatever it's doing) in the background. */
|
|
402
|
+
function switchChatTab(id) {
|
|
403
|
+
if (id === activeTab.current || !tabStore.current.has(id))
|
|
404
|
+
return;
|
|
405
|
+
tabStore.current.set(activeTab.current, { entries, streaming: streamed.current.text, busy, approval, queued, conversationId, agent: agentRef.current });
|
|
406
|
+
const t = tabStore.current.get(id);
|
|
407
|
+
tabStore.current.delete(id);
|
|
408
|
+
activeTab.current = id;
|
|
409
|
+
setActiveTabId(id);
|
|
410
|
+
agentRef.current = t.agent;
|
|
411
|
+
setEntries(t.entries);
|
|
412
|
+
showStream(t.streaming);
|
|
413
|
+
setBusy(t.busy);
|
|
414
|
+
setApproval(t.approval);
|
|
415
|
+
setQueued(t.queued);
|
|
416
|
+
setConversationId(t.conversationId);
|
|
417
|
+
setFocus("chat");
|
|
418
|
+
}
|
|
419
|
+
/** A new conversation tab, with the same engine and model, next to the others. */
|
|
420
|
+
function newChatTab() {
|
|
421
|
+
const id = String(Date.now());
|
|
422
|
+
const agent = createEngine(agentRef.current.id, choice, engineConfig.current.system ?? buildSystemPrompt(permContext), hooksFor(id));
|
|
423
|
+
agent.configure(engineConfig.current);
|
|
424
|
+
tabStore.current.set(id, { entries: [], streaming: "", busy: null, approval: null, queued: [], conversationId: newSessionId(), agent });
|
|
425
|
+
setChatTabs((tabs) => [...tabs, id]);
|
|
426
|
+
switchChatTab(id);
|
|
427
|
+
}
|
|
428
|
+
/** Closes the conversation tab on screen (stopping its engine); the conversation stays saved for /resume. */
|
|
429
|
+
function closeChatTab() {
|
|
430
|
+
if (chatTabs.length < 2)
|
|
431
|
+
return info("That's the only conversation tab. /clear starts a fresh conversation in it.");
|
|
432
|
+
const id = activeTab.current;
|
|
433
|
+
const at = chatTabs.indexOf(id);
|
|
434
|
+
const next = chatTabs[at + 1] ?? chatTabs[at - 1];
|
|
435
|
+
const agent = agentRef.current;
|
|
436
|
+
approval?.resolve(false);
|
|
437
|
+
switchChatTab(next);
|
|
438
|
+
agent.cancel();
|
|
439
|
+
agent.dispose?.();
|
|
440
|
+
tabStore.current.delete(id);
|
|
441
|
+
tabHooks.current.delete(id);
|
|
442
|
+
setChatTabs((tabs) => tabs.filter((t) => t !== id));
|
|
443
|
+
}
|
|
444
|
+
/** The tabs as the chat's title row shows them: named for what you asked first, marked when working or waiting. */
|
|
445
|
+
function chatTabItems() {
|
|
446
|
+
return chatTabs.map((id, i) => {
|
|
447
|
+
const t = id === activeTab.current ? { entries, busy, approval } : tabStore.current.get(id);
|
|
448
|
+
const first = t?.entries.find((e) => e.kind === "user")?.text.replace(/\s+/g, " ");
|
|
449
|
+
const label = first ? (first.length > 14 ? `${first.slice(0, 13)}…` : first) : `chat ${i + 1}`;
|
|
450
|
+
return { id, label, state: t?.approval ? "waiting" : t?.busy ? "busy" : "idle" };
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
function chatTabsProp() {
|
|
454
|
+
const items = chatTabItems();
|
|
455
|
+
const key = `${activeTabId}|${items.map((t) => `${t.id}:${t.label}:${t.state}`).join("|")}`;
|
|
456
|
+
if (chatTabsCache.current?.key !== key)
|
|
457
|
+
chatTabsCache.current = { key, tabs: { items, active: activeTabId } };
|
|
458
|
+
return chatTabsCache.current.tabs;
|
|
459
|
+
}
|
|
460
|
+
function saveTab(t) {
|
|
461
|
+
const first = t.entries.find((e) => e.kind === "user");
|
|
462
|
+
if (first)
|
|
463
|
+
saveConversation({ id: t.conversationId, title: first.text.replace(/\s+/g, " ").slice(0, 80), updated: Date.now(), engine: t.agent.id, entries: t.entries, state: t.agent.snapshot?.() });
|
|
464
|
+
}
|
|
327
465
|
if (!agentRef.current) {
|
|
328
466
|
const saved = loadSettings().engine;
|
|
329
|
-
agentRef.current = createEngine(isEngineId(saved) ? saved : "nou", initialModel, buildSystemPrompt(permContext),
|
|
467
|
+
agentRef.current = createEngine(isEngineId(saved) ? saved : "nou", initialModel, buildSystemPrompt(permContext), hooksFor(activeTab.current));
|
|
330
468
|
}
|
|
331
469
|
useEffect(() => diagnostics.subscribe(() => setDiagVersion((v) => v + 1)), []);
|
|
332
470
|
useEffect(() => onGrammarLoaded(() => setSyntaxVersion((v) => v + 1)), []);
|
|
@@ -398,7 +536,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
398
536
|
useEffect(() => {
|
|
399
537
|
const { extras, extraTools } = harness({ diagnostics, plugins: activePlugins.map((p) => p.plugin), git: settings.gitContext && repo });
|
|
400
538
|
// Other engines read AGENTS.md / CLAUDE.md themselves: they get only the NOU.md rules on top.
|
|
401
|
-
engineConfig.current = { system: buildSystemPrompt(permContext, extras), extras: [...extras, nouRulesOnly(permContext)].filter(Boolean), extraTools: [...extraTools, ...mcpRef.current.tools()], effort: settings.effort };
|
|
539
|
+
engineConfig.current = { system: buildSystemPrompt(permContext, extras), extras: [...extras, nouRulesOnly(permContext)].filter(Boolean), extraTools: [...extraTools, showStepsTool((steps) => (guideFound.current = steps)), ...mcpRef.current.tools()], effort: settings.effort };
|
|
402
540
|
agentRef.current.configure(engineConfig.current);
|
|
403
541
|
}, [settings.gitContext, settings.effort, activePlugins, mcpVersion]);
|
|
404
542
|
// Pick up changes made outside Nou (other editors, git commands).
|
|
@@ -466,7 +604,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
466
604
|
info(`Installed ${target.label} (${target.package}).`);
|
|
467
605
|
}
|
|
468
606
|
agentRef.current.dispose?.();
|
|
469
|
-
const engine = createEngine(id, choice, engineConfig.current.system ?? buildSystemPrompt(permContext),
|
|
607
|
+
const engine = createEngine(id, choice, engineConfig.current.system ?? buildSystemPrompt(permContext), hooksFor(activeTab.current));
|
|
470
608
|
engine.configure(engineConfig.current);
|
|
471
609
|
agentRef.current = engine;
|
|
472
610
|
updateSettings({ engine: id });
|
|
@@ -522,7 +660,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
522
660
|
let engine = agentRef.current;
|
|
523
661
|
if (id && id !== engine.id && isInstalled(engineInfo(id))) {
|
|
524
662
|
engine.dispose?.();
|
|
525
|
-
engine = createEngine(id, choice, engineConfig.current.system ?? buildSystemPrompt(permContext),
|
|
663
|
+
engine = createEngine(id, choice, engineConfig.current.system ?? buildSystemPrompt(permContext), hooksFor(activeTab.current));
|
|
526
664
|
engine.configure(engineConfig.current);
|
|
527
665
|
agentRef.current = engine;
|
|
528
666
|
updateSettings({ engine: id });
|
|
@@ -752,6 +890,18 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
752
890
|
// The Games panel sits under a pane: it takes the clicks that land on it, × on its title closes it,
|
|
753
891
|
// and dragging its title docks it under the pane you let go over.
|
|
754
892
|
const term = layout.current.terminal;
|
|
893
|
+
// Dragging the terminal's top border makes it taller or shorter; its bottom stays put.
|
|
894
|
+
if (term && dragging.current === "terminal") {
|
|
895
|
+
const rows = Math.max(4, term.y + term.height - event.y);
|
|
896
|
+
if (event.kind === "release") {
|
|
897
|
+
dragging.current = null;
|
|
898
|
+
setTermDrag(null);
|
|
899
|
+
updateSettings({ terminalRows: rows });
|
|
900
|
+
}
|
|
901
|
+
else if (event.kind === "drag")
|
|
902
|
+
setTermDrag(rows);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
755
905
|
if (term && event.x >= term.x && event.x < term.x + term.width && event.y >= term.y && event.y < term.y + term.height && !dragging.current) {
|
|
756
906
|
if (event.kind === "wheel")
|
|
757
907
|
return queueWheel("terminal", event.dir, false);
|
|
@@ -759,6 +909,10 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
759
909
|
return;
|
|
760
910
|
if (event.y === term.y + 1 && event.x >= term.x + term.width - 5)
|
|
761
911
|
return toggleTerminal(false); // ×
|
|
912
|
+
if (event.y === term.y) {
|
|
913
|
+
dragging.current = "terminal"; // its top border: drag to resize
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
762
916
|
if (focus !== "terminal")
|
|
763
917
|
beforeTerm.current = focus;
|
|
764
918
|
setFocus("terminal");
|
|
@@ -767,6 +921,18 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
767
921
|
const game = layout.current.game;
|
|
768
922
|
if (gamesOpen && game) {
|
|
769
923
|
const drag = dragging.current;
|
|
924
|
+
// Its top border makes it taller or shorter (its bottom stays put); its title moves it.
|
|
925
|
+
if (drag === "gameSize") {
|
|
926
|
+
const rows = Math.max(GAME_PANEL.minHeight, game.y + game.height - event.y);
|
|
927
|
+
if (event.kind === "release") {
|
|
928
|
+
dragging.current = null;
|
|
929
|
+
setGameSizeDrag(null);
|
|
930
|
+
updateSettings({ gameRows: rows });
|
|
931
|
+
}
|
|
932
|
+
else if (event.kind === "drag")
|
|
933
|
+
setGameSizeDrag(rows);
|
|
934
|
+
return;
|
|
935
|
+
}
|
|
770
936
|
if (drag === "game") {
|
|
771
937
|
const { treeWidth, codeWidth } = layout.current;
|
|
772
938
|
const over = event.x < treeWidth ? "tree" : event.x < treeWidth + codeWidth ? "code" : "chat";
|
|
@@ -793,7 +959,9 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
793
959
|
if (focus !== "game")
|
|
794
960
|
beforeGame.current = focus;
|
|
795
961
|
setFocus("game");
|
|
796
|
-
if (event.y
|
|
962
|
+
if (event.y === game.y)
|
|
963
|
+
dragging.current = "gameSize";
|
|
964
|
+
else if (event.y === game.y + 1)
|
|
797
965
|
dragging.current = "game";
|
|
798
966
|
return;
|
|
799
967
|
}
|
|
@@ -858,6 +1026,16 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
858
1026
|
scrollPane(pane, event.y);
|
|
859
1027
|
return;
|
|
860
1028
|
}
|
|
1029
|
+
// The chat's title row holds the conversation tabs: a click switches, + opens a new one.
|
|
1030
|
+
if (event.kind === "click" && pane === "chat" && event.y === 2) {
|
|
1031
|
+
const at = event.x - treeWidth - codeWidth - 1;
|
|
1032
|
+
const { zones, plus } = chatTabZones(chatTabItems());
|
|
1033
|
+
if (Math.abs(at - plus) <= 1)
|
|
1034
|
+
return newChatTab();
|
|
1035
|
+
const zone = zones.find((z) => at >= z.from && at < z.to);
|
|
1036
|
+
if (zone)
|
|
1037
|
+
return switchChatTab(zone.id);
|
|
1038
|
+
}
|
|
861
1039
|
// The code view's title row holds the tabs: a click switches, the × closes.
|
|
862
1040
|
if (event.kind === "click" && pane === "code" && event.y === 2 && tabs.length && !diff) {
|
|
863
1041
|
const at = event.x - codeLeft - 1;
|
|
@@ -1161,6 +1339,8 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1161
1339
|
openAtRef.current = openAt;
|
|
1162
1340
|
/** A file named in the chat (clicked, or shown by Nou): open it there, highlighting the lines it names. */
|
|
1163
1341
|
const openRef = useEvent((ref) => {
|
|
1342
|
+
if (ref.url)
|
|
1343
|
+
return openInBrowser(ref.url);
|
|
1164
1344
|
clearFollow();
|
|
1165
1345
|
const file = path.relative(process.cwd(), path.resolve(ref.path)).replace(/\\/g, "/");
|
|
1166
1346
|
openAt(file, ref.line ?? 1, false, ref.line ? ref.to ?? ref.line : undefined);
|
|
@@ -1536,6 +1716,19 @@ ${snippet}
|
|
|
1536
1716
|
return;
|
|
1537
1717
|
case "terminal":
|
|
1538
1718
|
return toggleTerminal();
|
|
1719
|
+
case "new":
|
|
1720
|
+
return newChatTab();
|
|
1721
|
+
case "close":
|
|
1722
|
+
return closeChatTab();
|
|
1723
|
+
case "tab": {
|
|
1724
|
+
const n = Number(arg);
|
|
1725
|
+
if (!n || !chatTabs[n - 1])
|
|
1726
|
+
return info(`Conversation tabs: ${chatTabItems().map((t, i) => `${i + 1}. ${t.label}${t.state !== "idle" ? ` (${t.state === "busy" ? "working" : "waiting on you"})` : ""}`).join(" ")}. /tab <n> switches, /new opens one, /close closes this one.`);
|
|
1727
|
+
return switchChatTab(chatTabs[n - 1]);
|
|
1728
|
+
}
|
|
1729
|
+
case "showme":
|
|
1730
|
+
case "teach":
|
|
1731
|
+
return showMe(rest.join(" "));
|
|
1539
1732
|
case "resume":
|
|
1540
1733
|
return resumeConversation(rest[0] ?? "");
|
|
1541
1734
|
case "plugins":
|
|
@@ -1640,13 +1833,104 @@ ${snippet}
|
|
|
1640
1833
|
setQueued(queued.slice(take));
|
|
1641
1834
|
send(queued.slice(0, take).join("\n\n"));
|
|
1642
1835
|
}, [busy, approval, queued]);
|
|
1836
|
+
// When Nou has finished looking, the guide starts at its first step.
|
|
1837
|
+
useEffect(() => {
|
|
1838
|
+
if (busy || !guideAsk.current)
|
|
1839
|
+
return;
|
|
1840
|
+
const task = guideAsk.current;
|
|
1841
|
+
let steps = guideFound.current;
|
|
1842
|
+
guideAsk.current = null;
|
|
1843
|
+
guideFound.current = null;
|
|
1844
|
+
// Other engines hand their steps back in a ```nou-steps block at the end of the reply: read it, and
|
|
1845
|
+
// show the reply without it.
|
|
1846
|
+
if (!steps?.length) {
|
|
1847
|
+
const reply = [...entries].reverse().find((e) => e.kind === "assistant");
|
|
1848
|
+
const found = reply && stepsFromReply(reply.text);
|
|
1849
|
+
if (found) {
|
|
1850
|
+
steps = found.steps;
|
|
1851
|
+
setEntries((prev) => prev.map((e) => (e === reply ? { ...e, text: found.rest || "Here's how." } : e)));
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
// Asked not to, an engine that edited anyway (Codex can't be stopped before it does) is put back.
|
|
1855
|
+
const edited = timeline.all.slice(guideMark.current).filter((c) => c.source === "nou" && !c.undone);
|
|
1856
|
+
if (edited.length) {
|
|
1857
|
+
undoCheckpoints([...edited].reverse());
|
|
1858
|
+
info("It edited files anyway, so those changes were put back: you're the one typing them.");
|
|
1859
|
+
}
|
|
1860
|
+
if (!steps?.length)
|
|
1861
|
+
return info("Nou didn't come back with steps to show. Try asking more specifically, e.g. /showme add a --verbose flag to the CLI", "error");
|
|
1862
|
+
info(`Here's how, in ${steps.length} step${steps.length > 1 ? "s" : ""}. You type each one in the editor: the dim text ahead of the cursor is what goes next, and Tab fills in the rest of a line. /showme next skips a step, /showme stop ends it.`);
|
|
1863
|
+
setGuide({ steps, at: 0, task });
|
|
1864
|
+
}, [busy]);
|
|
1865
|
+
// Each step: open its file in the editor where the code goes, and say what to type and why.
|
|
1866
|
+
useEffect(() => {
|
|
1867
|
+
if (!guide)
|
|
1868
|
+
return;
|
|
1869
|
+
const step = guide.steps[guide.at];
|
|
1870
|
+
if (!existsSync(step.file)) {
|
|
1871
|
+
// A step that starts a new file: it's created empty, for you to type into.
|
|
1872
|
+
mkdirSync(path.dirname(path.resolve(step.file)), { recursive: true });
|
|
1873
|
+
writeFileSync(step.file, "");
|
|
1874
|
+
setTreeVersion((v) => v + 1);
|
|
1875
|
+
}
|
|
1876
|
+
info(`👉 Step ${guide.at + 1} of ${guide.steps.length}: ${step.file}:${step.line}\n${step.why}\n\n\`\`\`\n${step.code}\n\`\`\``);
|
|
1877
|
+
openAt(step.file, step.line, true);
|
|
1878
|
+
}, [guide?.at, guide?.steps]);
|
|
1879
|
+
const guideStep = guide?.steps[guide.at] ?? null;
|
|
1880
|
+
/** The step's code is all typed: save it, and go on to the next one. */
|
|
1881
|
+
const onGuideDone = useEvent(() => {
|
|
1882
|
+
if (!guide || !guideStep)
|
|
1883
|
+
return;
|
|
1884
|
+
if (draft.current?.path === guideStep.file)
|
|
1885
|
+
saveFromEditor(draft.current.text, guideStep.file);
|
|
1886
|
+
// Later steps in the same file further down move down by the lines just added.
|
|
1887
|
+
const added = guideStep.code.split("\n").length;
|
|
1888
|
+
const steps = guide.steps.map((s, i) => (i > guide.at && s.file === guideStep.file && s.line >= guideStep.line ? { ...s, line: s.line + added } : s));
|
|
1889
|
+
if (guide.at + 1 < steps.length) {
|
|
1890
|
+
info(`✓ Step ${guide.at + 1} done.`);
|
|
1891
|
+
setGuide({ ...guide, steps, at: guide.at + 1 });
|
|
1892
|
+
}
|
|
1893
|
+
else {
|
|
1894
|
+
info(`🎉 That's it: all ${steps.length} step${steps.length > 1 ? "s" : ""} done and saved. Ask Nou to check it over if you like.`);
|
|
1895
|
+
setGuide(null);
|
|
1896
|
+
}
|
|
1897
|
+
});
|
|
1898
|
+
/** /showme <what you want>: Nou finds where the change goes and walks you through typing it. */
|
|
1899
|
+
const showMe = (arg) => {
|
|
1900
|
+
const words = arg.trim();
|
|
1901
|
+
if (words === "stop" || words === "end") {
|
|
1902
|
+
setGuide(null);
|
|
1903
|
+
return info(guide ? "Stopped the guide. What you typed so far stays." : "No guide is running.");
|
|
1904
|
+
}
|
|
1905
|
+
if (words === "next" || words === "skip") {
|
|
1906
|
+
if (!guide)
|
|
1907
|
+
return info("No guide is running.");
|
|
1908
|
+
if (guide.at + 1 >= guide.steps.length) {
|
|
1909
|
+
setGuide(null);
|
|
1910
|
+
return info("That was the last step.");
|
|
1911
|
+
}
|
|
1912
|
+
return setGuide({ ...guide, at: guide.at + 1 });
|
|
1913
|
+
}
|
|
1914
|
+
if (!words) {
|
|
1915
|
+
if (guide)
|
|
1916
|
+
return info(`On step ${guide.at + 1} of ${guide.steps.length}: ${guideStep.file}:${guideStep.line}. /showme next skips it, /showme stop ends it.`);
|
|
1917
|
+
return info("Tell Nou what you want to do and it shows you how, step by step, for you to type: /showme add a --verbose flag to the CLI");
|
|
1918
|
+
}
|
|
1919
|
+
if (busy)
|
|
1920
|
+
return info("Wait for the current reply to finish (or press Esc) first.", "error");
|
|
1921
|
+
setGuide(null);
|
|
1922
|
+
guideAsk.current = words;
|
|
1923
|
+
guideFound.current = null;
|
|
1924
|
+
guideMark.current = timeline.all.length;
|
|
1925
|
+
send(showMePrompt(words, agentRef.current.id === "nou"));
|
|
1926
|
+
};
|
|
1643
1927
|
useEffect(() => {
|
|
1644
1928
|
if (busy)
|
|
1645
1929
|
stopped.current = false;
|
|
1646
1930
|
}, [busy]);
|
|
1647
1931
|
/** From the chat: while Nou works, a message waits its turn, or (`now`) stops Nou and goes right away. */
|
|
1648
1932
|
const submit = (text, now = false) => {
|
|
1649
|
-
if (!busy)
|
|
1933
|
+
if (!busy || (text.startsWith("/") && runsWhileBusy(text)))
|
|
1650
1934
|
return send(text);
|
|
1651
1935
|
if (now) {
|
|
1652
1936
|
stop();
|
|
@@ -1768,13 +2052,17 @@ ${snippet}
|
|
|
1768
2052
|
const treeWidth = paneWidths ? Math.max(MIN_TREE, Math.round(columns * paneWidths.tree)) : Math.max(24, Math.floor(columns * 0.2));
|
|
1769
2053
|
const chatWidth = paneWidths ? Math.max(MIN_CHAT, Math.min(Math.round(columns * paneWidths.chat), columns - treeWidth - MIN_CODE)) : Math.max(34, Math.floor(columns * 0.3));
|
|
1770
2054
|
const codeWidth = Math.max(MIN_CODE, columns - treeWidth - chatWidth);
|
|
1771
|
-
const
|
|
2055
|
+
const tabKey = tabs.length && openFile ? `${tabs.join("\0")}|${openFile}|${tabsWidth(codeWidth)}|${showEditor && !!editorState?.dirty}` : "";
|
|
2056
|
+
if (tabStripCache.current?.key !== tabKey) {
|
|
2057
|
+
tabStripCache.current = { key: tabKey, strip: tabKey ? _jsx(Tabs, { tabs: tabs, active: openFile, width: tabsWidth(codeWidth), dirty: showEditor && !!editorState?.dirty }) : undefined };
|
|
2058
|
+
}
|
|
2059
|
+
const tabStrip = tabStripCache.current.strip;
|
|
1772
2060
|
// The Games panel takes the bottom of its pane's column: up to 19 rows, and never more than half.
|
|
1773
|
-
const gameHeight = gamesOpen ? Math.
|
|
2061
|
+
const gameHeight = gamesOpen ? Math.max(GAME_PANEL.minHeight, Math.min(gameSizeDrag ?? settings.gameRows ?? GAME_PANEL.height, height - 6)) : 0;
|
|
1774
2062
|
const heights = { tree: height, code: height, chat: height };
|
|
1775
2063
|
heights[gameDock] -= gameHeight;
|
|
1776
2064
|
// The terminal takes the bottom third of the code's column (above the Games panel when that's there too).
|
|
1777
|
-
const termHeight = termOpen ? Math.max(
|
|
2065
|
+
const termHeight = termOpen ? Math.max(4, Math.min(termDrag ?? settings.terminalRows ?? Math.floor(height / 3), heights.code - 4)) : 0;
|
|
1778
2066
|
heights.code -= termHeight;
|
|
1779
2067
|
const dockLeft = gameDock === "tree" ? 0 : gameDock === "code" ? treeWidth : treeWidth + codeWidth;
|
|
1780
2068
|
const dockWidth = gameDock === "tree" ? treeWidth : gameDock === "code" ? codeWidth : chatWidth;
|
|
@@ -1804,6 +2092,12 @@ ${snippet}
|
|
|
1804
2092
|
else if (focus === "terminal")
|
|
1805
2093
|
setFocus(beforeTerm.current === "terminal" ? "code" : beforeTerm.current);
|
|
1806
2094
|
}
|
|
2095
|
+
/** Alt+↑/↓ in the terminal: taller or shorter by a couple of rows. */
|
|
2096
|
+
function resizeTerminal(by) {
|
|
2097
|
+
const now = layout.current.terminal?.height ?? 0;
|
|
2098
|
+
if (now)
|
|
2099
|
+
updateSettings({ terminalRows: Math.max(4, now + by) });
|
|
2100
|
+
}
|
|
1807
2101
|
function leaveTerminal() {
|
|
1808
2102
|
setFocus(beforeTerm.current === "terminal" ? "code" : beforeTerm.current);
|
|
1809
2103
|
}
|
|
@@ -1958,9 +2252,9 @@ ${snippet}
|
|
|
1958
2252
|
}
|
|
1959
2253
|
const modelLabel = agentRef.current.id === "nou" ? describe(choice) : `${engineInfo(agentRef.current.id).label} · ${engineInfo(agentRef.current.id).model(choice)}`;
|
|
1960
2254
|
clickable.current = { status: zones, modelFrom: columns - (" ⌂ LOCAL ".length + ` ${modelLabel} `.length + 1) };
|
|
1961
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Header, { width: columns, branch: branch, changes: git.size, model: modelLabel, local: choice.provider.local, busy: busy, update: update }), _jsxs(Box, { flexDirection: "row", height: height, children: [_jsx(FileTree, { width: treeWidth, height: heights.tree, focused: focus === "tree", openFile: openFile, reveal: reveal, version: treeVersion, changed: edited, working: working, git: git, problemCounts: problemCounts, wheel: wheelFor("tree"), scrollTo: scrollFor("tree"), click: treeClick, onOpen: onTreeOpen, onHover: onTreeHover }), codePanel === "problems" && !pendingChange ? (_jsx(ProblemsView, { width: codeWidth, height: heights.code, focused: focus === "code", problems: diagnostics.all(), checking: checking, wheel: wheelFor("code"), onJump: onJump, onClose: onCloseProblems })) : showEditor ? (_jsx(Editor, { ...(tabStrip ? { tabs: tabStrip } : {}), width: codeWidth, height: heights.code, focused: focus === "code", filePath: openFile, initial: draft.current?.path === openFile ? draft.current.text : content, baseline: draft.current?.path === openFile ? draft.current.baseline : content, startLine: editorStart.line, ...(editorStart.col ? { startCol: editorStart.col } : {}), problems: fileProblems, autoSave: settings.autoSave, syntaxVersion: syntaxVersion, wheel: wheelFor("code"), scrollTo: scrollFor("code"), mouse: editorMouse, onHover: onEditorHover, onDefinition: onEditorDefinition, onSave: onEditorSave, onExit: onEditorExit, onState: onEditorState }, `${openFile}:${editorStart.nonce}`)) : (_jsx(CodeView, { ...(tabStrip && !diff ? { tabs: tabStrip } : {}), width: codeWidth, height: heights.code, focused: focus === "code", filePath: openFile, content: content, diff: diff, diffLabel: diffMode === "git" ? "vs HEAD" : diffMode === "session" ? sessionView?.label ?? "this session" : "Nou's edit", pending: !!pendingChange && diff === pendingChange, marked: record?.lines ?? NO_LINES, problems: fileProblems, focusLine: focusLine, syntaxVersion: syntaxVersion, diffNote: pendingChange && diff === pendingChange ? pendingNote?.short ?? (checking.length ? "checking…" : null) : null, wheel: wheelFor("code"), scrollTo: scrollFor("code"), onViewport: onViewport, defineAt: codeDefine, onDefine: onCodeDefine, onFinding: onCodeFinding, findRequest: findRequest })), _jsx(Chat, { width: chatWidth, height: heights.chat, focused: focus === "chat", entries: entries, streaming: streaming, busy: busy, approval: approval?.request ?? null, approvalNote: pendingChange ? pendingNote?.detail ?? null : null, contextChip: contextChip, wheel: wheelFor("chat"), scrollTo: scrollFor("chat"), prefill: chatPrefill, extraCommands: pluginCommands, onMenu: onChatMenu, onSubmit: onSend, queued: queued, onAnswer: onAnswer, click: chatClick, select: chatSelect, onOpenRef: openRef })] }), _jsx(Box, { children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { backgroundColor: mode.color, color: "black", bold: true, children: ` ${mode.label} ` }), _jsx(Text, { backgroundColor: C.panel, color: autoMode === "ask" ? C.dim : C.orange, bold: autoMode !== "ask", children: ` ${autoMode === "ask" ? "ask" : autoMode} ` }), _jsx(Text, { backgroundColor: C.panel, color: follow ? C.green : C.dim, children: followChip }), _jsx(Text, { backgroundColor: C.panel, color: totals.errors ? C.red : C.dim, children: ` ✖ ${totals.errors}` }), _jsx(Text, { backgroundColor: C.panel, color: totals.warnings ? C.yellow : C.dim, children: ` ▲ ${totals.warnings} ` }), checking.length > 0 && _jsxs(Text, { color: C.accent2, children: [" \u27F3 ", checking.join(", ")] }), exitArmed
|
|
2255
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, children: [_jsx(Header, { width: columns, branch: branch, changes: git.size, model: modelLabel, local: choice.provider.local, busy: busy, update: update }), _jsxs(Box, { flexDirection: "row", height: height, children: [_jsx(FileTree, { width: treeWidth, height: heights.tree, focused: focus === "tree", openFile: openFile, reveal: reveal, version: treeVersion, changed: edited, working: working, git: git, problemCounts: problemCounts, wheel: wheelFor("tree"), scrollTo: scrollFor("tree"), click: treeClick, onOpen: onTreeOpen, onHover: onTreeHover }), codePanel === "problems" && !pendingChange ? (_jsx(ProblemsView, { width: codeWidth, height: heights.code, focused: focus === "code", problems: diagnostics.all(), checking: checking, wheel: wheelFor("code"), onJump: onJump, onClose: onCloseProblems })) : showEditor ? (_jsx(Editor, { ...(tabStrip ? { tabs: tabStrip } : {}), width: codeWidth, height: heights.code, focused: focus === "code", filePath: openFile, initial: draft.current?.path === openFile ? draft.current.text : content, baseline: draft.current?.path === openFile ? draft.current.baseline : content, startLine: editorStart.line, ...(editorStart.col ? { startCol: editorStart.col } : {}), problems: fileProblems, autoSave: settings.autoSave, syntaxVersion: syntaxVersion, wheel: wheelFor("code"), scrollTo: scrollFor("code"), mouse: editorMouse, onHover: onEditorHover, onDefinition: onEditorDefinition, onSave: onEditorSave, onExit: onEditorExit, onState: onEditorState, ...(guideStep && openFile === guideStep.file ? { guide: { row: guideStep.line - 1, code: guideStep.code }, onGuideDone } : {}) }, `${openFile}:${editorStart.nonce}`)) : (_jsx(CodeView, { ...(tabStrip && !diff ? { tabs: tabStrip } : {}), width: codeWidth, height: heights.code, focused: focus === "code", filePath: openFile, content: content, diff: diff, diffLabel: diffMode === "git" ? "vs HEAD" : diffMode === "session" ? sessionView?.label ?? "this session" : "Nou's edit", pending: !!pendingChange && diff === pendingChange, marked: record?.lines ?? NO_LINES, problems: fileProblems, focusLine: focusLine, syntaxVersion: syntaxVersion, diffNote: pendingChange && diff === pendingChange ? pendingNote?.short ?? (checking.length ? "checking…" : null) : null, wheel: wheelFor("code"), scrollTo: scrollFor("code"), onViewport: onViewport, defineAt: codeDefine, onDefine: onCodeDefine, onFinding: onCodeFinding, findRequest: findRequest })), _jsx(Chat, { width: chatWidth, height: heights.chat, focused: focus === "chat", entries: entries, streaming: streaming, busy: busy, approval: approval?.request ?? null, approvalNote: pendingChange ? pendingNote?.detail ?? null : null, contextChip: contextChip, wheel: wheelFor("chat"), scrollTo: scrollFor("chat"), prefill: chatPrefill, extraCommands: pluginCommands, onMenu: onChatMenu, onSubmit: onSend, queued: queued, onAnswer: onAnswer, click: chatClick, select: chatSelect, tabs: chatTabsProp(), onOpenRef: openRef })] }), _jsx(Box, { children: _jsxs(Text, { wrap: "truncate-end", children: [_jsx(Text, { backgroundColor: mode.color, color: "black", bold: true, children: ` ${mode.label} ` }), _jsx(Text, { backgroundColor: C.panel, color: autoMode === "ask" ? C.dim : C.orange, bold: autoMode !== "ask", children: ` ${autoMode === "ask" ? "ask" : autoMode} ` }), _jsx(Text, { backgroundColor: C.panel, color: follow ? C.green : C.dim, children: followChip }), _jsx(Text, { backgroundColor: C.panel, color: totals.errors ? C.red : C.dim, children: ` ✖ ${totals.errors}` }), _jsx(Text, { backgroundColor: C.panel, color: totals.warnings ? C.yellow : C.dim, children: ` ▲ ${totals.warnings} ` }), checking.length > 0 && _jsxs(Text, { color: C.accent2, children: [" \u27F3 ", checking.join(", ")] }), exitArmed
|
|
1962
2256
|
? _jsxs(Text, { color: C.yellow, bold: true, children: [" ", "Press again to quit", draft.current && settings.autoSave === "off" ? ` (unsaved changes in ${draft.current.path} will be lost)` : ""] })
|
|
1963
|
-
: shownHints.map(([key, action], i) => (_jsxs(Text, { children: [i === 0 ? " " : _jsx(Text, { color: C.dim, children: " \u00B7 " }), _jsx(Text, { color: C.text, children: key }), _jsxs(Text, { color: C.dim, children: [" ", action] })] }, key)))] }) }), layout.current.terminal && shellRef.current && (_jsx(Box, { position: "absolute", marginLeft: layout.current.terminal.x, marginTop: layout.current.terminal.y, children: _jsx(Terminal, { shell: shellRef.current, version: termVersion, width: layout.current.terminal.width, height: layout.current.terminal.height, focused: focus === "terminal", wheel: wheelFor("terminal"), onLeave: leaveTerminal }) })), layout.current.game && (_jsx(Box, { position: "absolute", marginLeft: layout.current.game.x, marginTop: layout.current.game.y, children: _jsx(GamePanel, { width: layout.current.game.width, height: layout.current.game.height, focused: focus === "game", moving: !!gameDrag, installed: settings.games, onLeave: leaveGames, onOpenSettings: gamesSettings }) })), quickOpen !== null && (_jsx(QuickOpen, { width: columns, top: 3, files: allFiles, recent: recent, actions: paletteActions, initialQuery: quickOpen, onPick: (file) => {
|
|
2257
|
+
: shownHints.map(([key, action], i) => (_jsxs(Text, { children: [i === 0 ? " " : _jsx(Text, { color: C.dim, children: " \u00B7 " }), _jsx(Text, { color: C.text, children: key }), _jsxs(Text, { color: C.dim, children: [" ", action] })] }, key)))] }) }), layout.current.terminal && shellRef.current && (_jsx(Box, { position: "absolute", marginLeft: layout.current.terminal.x, marginTop: layout.current.terminal.y, children: _jsx(Terminal, { shell: shellRef.current, version: termVersion, width: layout.current.terminal.width, height: layout.current.terminal.height, focused: focus === "terminal", wheel: wheelFor("terminal"), onLeave: leaveTerminal, onResize: resizeTerminal }) })), layout.current.game && (_jsx(Box, { position: "absolute", marginLeft: layout.current.game.x, marginTop: layout.current.game.y, children: _jsx(GamePanel, { width: layout.current.game.width, height: layout.current.game.height, focused: focus === "game", moving: !!gameDrag, installed: settings.games, onLeave: leaveGames, onOpenSettings: gamesSettings }) })), quickOpen !== null && (_jsx(QuickOpen, { width: columns, top: 3, files: allFiles, recent: recent, actions: paletteActions, initialQuery: quickOpen, onPick: (file) => {
|
|
1964
2258
|
setQuickOpen(null);
|
|
1965
2259
|
clearFollow();
|
|
1966
2260
|
open(file);
|