nou-cli 0.5.4 → 0.5.10
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 +18 -7
- package/dist/src/agent/index.js +1 -0
- package/dist/src/cli.js +69 -0
- package/dist/src/config/index.js +20 -0
- package/dist/src/conversations/external.js +35 -13
- package/dist/src/diagnostics/providers.js +23 -2
- package/dist/src/engines/codex.js +28 -1
- package/dist/src/games/index.js +220 -30
- package/dist/src/guide/index.js +75 -0
- package/dist/src/ui/App.js +281 -36
- package/dist/src/ui/Chat.js +63 -11
- package/dist/src/ui/CodeView.js +71 -20
- package/dist/src/ui/Editor.js +1 -1
- package/dist/src/ui/FileTree.js +3 -1
- package/dist/src/ui/GamePanel.js +42 -14
- package/dist/src/ui/Header.js +32 -4
- package/dist/src/ui/Pane.js +2 -2
- package/dist/src/ui/ProblemsView.js +3 -1
- package/dist/src/ui/QuickOpen.js +8 -3
- package/dist/src/ui/Settings.js +21 -7
- package/dist/src/ui/Terminal.js +7 -1
- package/dist/src/ui/background.js +58 -0
- package/dist/src/ui/commands.js +49 -7
- package/dist/src/ui/focus.js +59 -0
- package/dist/src/ui/redraw.js +16 -0
- package/dist/src/ui/render-options.js +3 -0
- package/dist/src/ui/text.js +45 -0
- package/dist/src/ui/theme.js +59 -13
- package/dist/src/workspaces/client.js +69 -0
- package/dist/src/workspaces/host.js +144 -0
- package/package.json +2 -1
package/dist/src/ui/App.js
CHANGED
|
@@ -13,7 +13,7 @@ import { ENGINES, createEngine, engineInfo, installEngine, isEngineId, isInstall
|
|
|
13
13
|
import { harness, problemsIntroduced } from "../agent/harness.js";
|
|
14
14
|
import { describe, providers, resolveModel, saveModel, setupProblem } from "../providers/index.js";
|
|
15
15
|
import { sameModel } from "../providers/ollama.js";
|
|
16
|
-
import { FOLLOW_SPEEDS, isTrustedFolder, loadSettings, saveSettings } from "../config/index.js";
|
|
16
|
+
import { EFFORT_LABELS, EFFORTS, FOLLOW_SPEEDS, effortFrom, isTrustedFolder, loadSettings, saveSettings } from "../config/index.js";
|
|
17
17
|
import { branch as gitBranch, diffAgainstHead, isRepo, sameStatus, status as gitStatus, statusAsync as gitStatusAsync } from "../git/index.js";
|
|
18
18
|
import { BUILTIN_PLUGINS, addStarter, discoverPlugins, isEnabled, loadPlugin, scaffoldPlugin, starterPlugins, trustPatch, trustState } from "../plugins/index.js";
|
|
19
19
|
import { DiagnosticsManager, formatDiagnostic, newProblems } from "../diagnostics/index.js";
|
|
@@ -22,23 +22,27 @@ import { lsp } from "../lsp/index.js";
|
|
|
22
22
|
import { onGrammarLoaded, preload } from "../syntax/index.js";
|
|
23
23
|
import { setCodeTheme } from "../syntax/themes.js";
|
|
24
24
|
import { checkForUpdate, currentVersion } from "../update/index.js";
|
|
25
|
-
import { changedLines, lineCounts, loadFallbackHighlighter } from "./text.js";
|
|
25
|
+
import { changedLines, lineCounts, loadFallbackHighlighter, splitLines } from "./text.js";
|
|
26
26
|
import { findDefinition, wordAt } from "./definition.js";
|
|
27
27
|
import { ProblemsView } from "./ProblemsView.js";
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
28
|
+
import { useFocusReporting, useTerminalFocused } from "./focus.js";
|
|
29
|
+
import { ENABLE as MOUSE_ON, useMouse } from "./mouse.js";
|
|
30
|
+
import { connectToHost, isHosted, reopenAsWorkspaces, sendToHost, useWorkspaceTabs, workspaceTabs } from "../workspaces/client.js";
|
|
31
|
+
import { PTY_PACKAGE, workspaceFolder } from "../workspaces/host.js";
|
|
32
|
+
import { C, palette } from "./theme.js";
|
|
30
33
|
import { FileTree } from "./FileTree.js";
|
|
31
34
|
import { CodeView } from "./CodeView.js";
|
|
32
35
|
import { Editor } from "./Editor.js";
|
|
33
|
-
import { SLASH_COMMANDS, runsWhileBusy } from "./commands.js";
|
|
36
|
+
import { SLASH_COMMANDS, closestCommand, onlyChangesScreen, runsWhileBusy } from "./commands.js";
|
|
34
37
|
import { WHEEL_STEP } from "./scroll.js";
|
|
35
38
|
import { Chat, chatTabZones } from "./Chat.js";
|
|
36
39
|
import { copyToClipboard, openInBrowser } from "./clipboard.js";
|
|
37
|
-
import { Header } from "./Header.js";
|
|
40
|
+
import { Header, headerZones } from "./Header.js";
|
|
38
41
|
import { Tabs, layoutTabs } from "./Tabs.js";
|
|
39
|
-
import { GAME_PANEL, GamePanel } from "./GamePanel.js";
|
|
42
|
+
import { GAME_PANEL, GameGuard, GamePanel } from "./GamePanel.js";
|
|
40
43
|
import { Terminal } from "./Terminal.js";
|
|
41
|
-
import {
|
|
44
|
+
import { forgetScreen } from "./redraw.js";
|
|
45
|
+
import { SHOW_PLACES_NOTE, explainPrompt, showMePrompt, showPlacesTool, showStepsTool, stepsFromReply, stopsFromReply } from "../guide/index.js";
|
|
42
46
|
import { Shell } from "../terminal/index.js";
|
|
43
47
|
import { QuickOpen, listAllFiles, score } from "./QuickOpen.js";
|
|
44
48
|
import { Settings } from "./Settings.js";
|
|
@@ -84,7 +88,7 @@ Keys:
|
|
|
84
88
|
Ctrl+click, Option+click or right-click a name: go to its definition (an import's path opens the file)
|
|
85
89
|
p problems u undo Nou's edit s settings
|
|
86
90
|
c (in files) show only changed files Ctrl+C twice quit
|
|
87
|
-
Mouse: wheel scrolls (Shift+wheel sideways) · click/drag a scrollbar · double-click in the code to edit there (in the editor: select a word), drag to select
|
|
91
|
+
Mouse: wheel scrolls (Shift+wheel sideways) · click/drag a scrollbar · double-click in the code to edit there (in the editor: select a word), drag to select · in the chat: double-click copies a word, triple-click a paragraph
|
|
88
92
|
In the editor:
|
|
89
93
|
Shift+arrows / mouse drag select · Ctrl+A all · Ctrl+C/X/V copy/cut/paste · Esc clear selection or leave
|
|
90
94
|
Ctrl+←/→ word · Ctrl+Backspace or Ctrl+W delete word · Ctrl+Home/End file start/end
|
|
@@ -108,13 +112,17 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
108
112
|
const { exit } = useApp();
|
|
109
113
|
const { columns, rows } = useWindowSize();
|
|
110
114
|
const [settings, setSettings] = useState(loadSettings);
|
|
111
|
-
|
|
115
|
+
// Before anything below highlights code. One Dark's light text is unreadable on a light terminal: there the
|
|
116
|
+
// default switches to GitHub Light (any other theme you picked stays).
|
|
117
|
+
setCodeTheme(palette() === "light" && settings.codeTheme === "one-dark" ? "github-light" : settings.codeTheme);
|
|
112
118
|
const [view, setView] = useState("main");
|
|
113
119
|
const [connectTo, setConnectTo] = useState();
|
|
114
120
|
/** Why settings were opened for you (a request failed on the provider's setup). */
|
|
115
121
|
const [setupReason, setSetupReason] = useState(null);
|
|
116
122
|
/** The file finder / command palette: closed (null), or open with this query (">" for commands). */
|
|
117
123
|
const [quickOpen, setQuickOpen] = useState(null);
|
|
124
|
+
/** /resume's picker: the conversations, in the palette's place while it's open. */
|
|
125
|
+
const [resumePicker, setResumePicker] = useState(null);
|
|
118
126
|
const [focus, setFocus] = useState("chat");
|
|
119
127
|
/** The Games panel: whether it's open, the pane it's being dragged over, and the pane to go back to. */
|
|
120
128
|
const [gamesOpen, setGamesOpen] = useState(false);
|
|
@@ -127,19 +135,29 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
127
135
|
const [termVersion, setTermVersion] = useState(0);
|
|
128
136
|
/** Its height while its top border is being dragged (saved when you let go). */
|
|
129
137
|
const [termDrag, setTermDrag] = useState(null);
|
|
138
|
+
/** Bumped to draw every line again after the screen is cleared (/redraw, Ctrl+L). */
|
|
139
|
+
const [, setRepaint] = useState(0);
|
|
130
140
|
/** The Games panel's height while its top border is being dragged (saved when you let go). */
|
|
131
141
|
const [gameSizeDrag, setGameSizeDrag] = useState(null);
|
|
132
142
|
/** The tab strip, kept while nothing in it changes: a new one would redraw the whole code view on every scroll. */
|
|
133
143
|
const tabStripCache = useRef(null);
|
|
134
144
|
/**
|
|
135
|
-
* "
|
|
145
|
+
* "Teach me" (/teach): the steps Nou found, and the one you're on. While Nou looks for them,
|
|
136
146
|
* `guideAsk` holds what you asked (and its edits are turned down: you're doing the typing).
|
|
137
147
|
*/
|
|
138
148
|
const [guide, setGuide] = useState(null);
|
|
139
149
|
const guideAsk = useRef(null);
|
|
140
150
|
/** Where the undo history stood when the steps were asked for: an engine that edits anyway is put back. */
|
|
141
151
|
const guideMark = useRef(0);
|
|
152
|
+
/** /resume just listed the conversations: the next message, if it's a number, picks one. */
|
|
153
|
+
const pickingResume = useRef(false);
|
|
142
154
|
const guideFound = useRef(null);
|
|
155
|
+
/**
|
|
156
|
+
* "Show me" (/showme, or any explanation that points at code): the places the agent pointed at, and the
|
|
157
|
+
* one open in the code view. `tourFound` holds what Nou's engine passed to show_places during a turn.
|
|
158
|
+
*/
|
|
159
|
+
const [tour, setTour] = useState(null);
|
|
160
|
+
const tourFound = useRef(null);
|
|
143
161
|
const shellRef = useRef(null);
|
|
144
162
|
const beforeTerm = useRef("code");
|
|
145
163
|
const [follow, setFollow] = useState(settings.followMode);
|
|
@@ -201,6 +219,9 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
201
219
|
const [plugins, setPlugins] = useState([]);
|
|
202
220
|
const [starters, setStarters] = useState([]);
|
|
203
221
|
const [viewport, setViewport] = useState(null);
|
|
222
|
+
/** Folding in the code view: a click on a row's fold marker, and /fold, /unfold for the whole file. */
|
|
223
|
+
const [foldClick, setFoldClick] = useState(null);
|
|
224
|
+
const [foldAll, setFoldAll] = useState(null);
|
|
204
225
|
// The editor reports every keystroke; the app only re-renders when what it shows changes (line, unsaved, selection).
|
|
205
226
|
const editorStateRef = useRef(null);
|
|
206
227
|
const [editorState, setEditorState] = useState(null);
|
|
@@ -536,15 +557,21 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
536
557
|
useEffect(() => {
|
|
537
558
|
const { extras, extraTools } = harness({ diagnostics, plugins: activePlugins.map((p) => p.plugin), git: settings.gitContext && repo });
|
|
538
559
|
// Other engines read AGENTS.md / CLAUDE.md themselves: they get only the NOU.md rules on top.
|
|
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 };
|
|
560
|
+
engineConfig.current = { system: buildSystemPrompt(permContext, extras), extras: [...extras, nouRulesOnly(permContext), SHOW_PLACES_NOTE].filter(Boolean), extraTools: [...extraTools, showStepsTool((steps) => (guideFound.current = steps)), showPlacesTool((stops) => (tourFound.current = stops)), ...mcpRef.current.tools()], effort: settings.effort };
|
|
540
561
|
agentRef.current.configure(engineConfig.current);
|
|
541
562
|
}, [settings.gitContext, settings.effort, activePlugins, mcpVersion]);
|
|
542
|
-
// Pick up changes made outside Nou (other editors, git commands).
|
|
563
|
+
// Pick up changes made outside Nou (other editors, git commands). Not while Nou's window is in the
|
|
564
|
+
// background (in a big repo each `git status` costs real CPU); coming back checks right away.
|
|
543
565
|
const polling = useRef(false);
|
|
566
|
+
const wasFocused = useRef(true);
|
|
567
|
+
const windowFocused = useTerminalFocused();
|
|
568
|
+
useFocusReporting(true);
|
|
544
569
|
useEffect(() => {
|
|
545
|
-
|
|
570
|
+
const cameBack = windowFocused && !wasFocused.current;
|
|
571
|
+
wasFocused.current = windowFocused;
|
|
572
|
+
if (!repo || !windowFocused)
|
|
546
573
|
return;
|
|
547
|
-
const
|
|
574
|
+
const poll = () => {
|
|
548
575
|
if (busy || polling.current)
|
|
549
576
|
return;
|
|
550
577
|
polling.current = true;
|
|
@@ -553,9 +580,12 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
553
580
|
// Only redraw when something changed.
|
|
554
581
|
setGit((prev) => (sameStatus(prev, next) ? prev : next));
|
|
555
582
|
});
|
|
556
|
-
}
|
|
583
|
+
};
|
|
584
|
+
if (cameBack)
|
|
585
|
+
poll();
|
|
586
|
+
const timer = setInterval(poll, 4000);
|
|
557
587
|
return () => clearInterval(timer);
|
|
558
|
-
}, [repo, busy]);
|
|
588
|
+
}, [repo, busy, windowFocused]);
|
|
559
589
|
useEffect(() => {
|
|
560
590
|
if (busy)
|
|
561
591
|
return;
|
|
@@ -563,6 +593,24 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
563
593
|
if (openFile && diffMode === "off" && !editing)
|
|
564
594
|
setContent(readText(openFile));
|
|
565
595
|
}, [busy]);
|
|
596
|
+
/**
|
|
597
|
+
* Workspaces: when this Nou is one tab of several, the host says when it's on screen again (redraw all of
|
|
598
|
+
* it, and turn the mouse and focus reports back on, which another workspace may have turned off on its way
|
|
599
|
+
* out), and hears when it's busy, for the tab's ● .
|
|
600
|
+
*/
|
|
601
|
+
const tabsOfWorkspaces = useWorkspaceTabs();
|
|
602
|
+
useEffect(() => {
|
|
603
|
+
connectToHost({
|
|
604
|
+
redraw: () => {
|
|
605
|
+
process.stdout.write(`\x1b[?1049h${settings.mouse ? MOUSE_ON : ""}\x1b[?1004h`);
|
|
606
|
+
redraw();
|
|
607
|
+
},
|
|
608
|
+
error: (text) => info(text, "error"),
|
|
609
|
+
});
|
|
610
|
+
}, []);
|
|
611
|
+
useEffect(() => {
|
|
612
|
+
sendToHost({ type: "state", busy: !!busy });
|
|
613
|
+
}, [!!busy]);
|
|
566
614
|
const updateSettings = (patch) => setSettings(saveSettings(patch));
|
|
567
615
|
const switchModel = (next) => {
|
|
568
616
|
const problem = next.provider.unavailable();
|
|
@@ -624,18 +672,31 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
624
672
|
const list = resumable();
|
|
625
673
|
if (!list.length)
|
|
626
674
|
return info("No earlier conversations in this folder yet: Nou's are saved after each reply, and Claude Code's and Codex's own sessions here show up too.");
|
|
675
|
+
const where = (c) => ("file" in c ? `${engineInfo(c.engine).label} CLI` : engineInfo(isEngineId(c.engine) ? c.engine : "nou").label);
|
|
627
676
|
if (!arg) {
|
|
628
|
-
const rows = list.slice(0, 10).map((c, i) => ` ${String(i + 1).padStart(2)}. ${c.title} (${c.messages} message${c.messages === 1 ? "" : "s"} · ${
|
|
629
|
-
|
|
677
|
+
const rows = list.slice(0, 10).map((c, i) => ` ${String(i + 1).padStart(2)}. ${c.title} (${c.messages} message${c.messages === 1 ? "" : "s"} · ${where(c)} · ${ago(c.updated)})`);
|
|
678
|
+
pickingResume.current = true; // a bare number, typed next, picks one
|
|
679
|
+
info(`Earlier conversations here (also in the picker: ↑↓ and Enter, or type a number here):\n${rows.join("\n")}`);
|
|
680
|
+
// A picker over the screen: ↑↓ and Enter, or type to filter by title.
|
|
681
|
+
setResumePicker(list.map((c, i) => ({ title: `${i + 1}. ${c.title}`, keys: `${where(c)} · ${ago(c.updated)}`, run: () => void resumePicked(c) })));
|
|
682
|
+
setQuickOpen(">");
|
|
683
|
+
return;
|
|
630
684
|
}
|
|
631
685
|
const pick = arg === "last" ? list[0] : list[Number(arg) - 1] ?? list.find((c) => c.id === arg);
|
|
632
686
|
if (!pick)
|
|
633
687
|
return info(`There's no conversation ${arg}: /resume lists them.`, "error");
|
|
688
|
+
return resumePicked(pick);
|
|
689
|
+
};
|
|
690
|
+
/** Picks a conversation back up: Nou's own, or a Claude Code or Codex CLI session. */
|
|
691
|
+
const resumePicked = async (pick) => {
|
|
692
|
+
pickingResume.current = false;
|
|
693
|
+
if (busy)
|
|
694
|
+
return info("Wait for the current reply to finish (or press Esc) before resuming another conversation.", "error");
|
|
634
695
|
if ("file" in pick)
|
|
635
696
|
return resumeExternal(pick);
|
|
636
697
|
const saved = loadConversation(pick.id);
|
|
637
698
|
if (!saved)
|
|
638
|
-
return info(`Couldn't read conversation ${
|
|
699
|
+
return info(`Couldn't read conversation "${pick.title}".`, "error");
|
|
639
700
|
const engine = engineFor(isEngineId(saved.engine) ? saved.engine : null);
|
|
640
701
|
const restored = saved.engine === engine.id && !!saved.state && !!engine.restore?.(saved.state);
|
|
641
702
|
if (!restored) {
|
|
@@ -1001,6 +1062,14 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1001
1062
|
}
|
|
1002
1063
|
if (event.kind === "click" && !approval && event.y === 0 && event.x >= clickable.current.modelFrom)
|
|
1003
1064
|
return setView("settings");
|
|
1065
|
+
// Workspace tabs in the header: click one to switch to it, + to open another.
|
|
1066
|
+
if (event.kind === "click" && event.y === 0) {
|
|
1067
|
+
const zone = headerZones(workspaceTabs()).find((z) => event.x >= z.from && event.x < z.to);
|
|
1068
|
+
if (zone?.tab === "new")
|
|
1069
|
+
return typeCommand("/workspace new ");
|
|
1070
|
+
if (typeof zone?.tab === "number")
|
|
1071
|
+
return void sendToHost({ type: "switch", index: zone.tab });
|
|
1072
|
+
}
|
|
1004
1073
|
if (event.kind === "click" && event.y === paneHeight + 1)
|
|
1005
1074
|
return clickable.current.status.find((z) => event.x >= z.from && event.x < z.to)?.run();
|
|
1006
1075
|
if (event.y < 1 || event.y > paneHeight)
|
|
@@ -1078,17 +1147,32 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1078
1147
|
}
|
|
1079
1148
|
// Two clicks in the same spot within half a second are a double-click.
|
|
1080
1149
|
const last = lastClick.current;
|
|
1081
|
-
const
|
|
1082
|
-
|
|
1150
|
+
const again = !!last && Date.now() - last.time < DOUBLE_CLICK_MS && Math.abs(last.x - event.x) <= 1 && last.y === event.y;
|
|
1151
|
+
const count = again ? last.count + 1 : 1;
|
|
1152
|
+
const double = count === 2;
|
|
1153
|
+
// The chat counts a third click too (the whole paragraph); elsewhere a double-click starts over.
|
|
1154
|
+
lastClick.current = double && pane !== "chat" ? null : count >= 3 ? null : { x: event.x, y: event.y, time: Date.now(), count };
|
|
1155
|
+
// In the chat: a double-click selects and copies the word (or path, or link), a triple-click the paragraph.
|
|
1156
|
+
if (pane === "chat" && event.y >= 3 && count >= 2)
|
|
1157
|
+
chatAt(event.x, event.y, count === 2 ? "word" : "block");
|
|
1083
1158
|
if (pane === "code" && event.y >= 3 && codePanel === "code") {
|
|
1159
|
+
// As the code view counts them (a last empty line after the final newline isn't shown).
|
|
1160
|
+
const digits = String(splitLines(content).length).length;
|
|
1084
1161
|
if (showEditor) {
|
|
1085
1162
|
dragging.current = "editor";
|
|
1086
1163
|
editorAt(event.x, event.y, double ? "double" : "press");
|
|
1087
1164
|
}
|
|
1165
|
+
else if (openFile && !diff && event.x - codeLeft - 1 === digits + 1) {
|
|
1166
|
+
// The fold marker (▾/▸) beside the line number: fold or unfold that block.
|
|
1167
|
+
lastClick.current = null;
|
|
1168
|
+
setFoldClick((f) => ({ row: event.y - 3, nonce: (f?.nonce ?? 0) + 1 }));
|
|
1169
|
+
}
|
|
1088
1170
|
else if (double && openFile && !diff && settleDraft(openFile)) {
|
|
1089
1171
|
// Double-click in the code: edit right there.
|
|
1090
|
-
|
|
1091
|
-
|
|
1172
|
+
// The code view's gutter: the line number, a marker and the fold arrow; folds hide lines between rows.
|
|
1173
|
+
const gutter = digits + 2;
|
|
1174
|
+
const line = viewport?.lines?.[event.y - 3] ?? (viewport?.top ?? 1) + event.y - 3;
|
|
1175
|
+
setEditorStart((s) => ({ line, col: Math.max(1, event.x - codeLeft - gutter), nonce: s.nonce + 1 }));
|
|
1092
1176
|
setDiffMode("off");
|
|
1093
1177
|
setEditing(true);
|
|
1094
1178
|
}
|
|
@@ -1414,6 +1498,8 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1414
1498
|
setFocus("chat");
|
|
1415
1499
|
return;
|
|
1416
1500
|
}
|
|
1501
|
+
if (key.ctrl && input === "l")
|
|
1502
|
+
return redraw(); // (the editor and terminal use Ctrl+L themselves)
|
|
1417
1503
|
if (focus === "chat")
|
|
1418
1504
|
return; // letters belong to the text box
|
|
1419
1505
|
if (input === "?")
|
|
@@ -1602,6 +1688,21 @@ ${snippet}
|
|
|
1602
1688
|
return info(`No file matches "${wanted}". To find where something is in the code, ask in the chat.`, "error");
|
|
1603
1689
|
return openRef({ path: file, ...(m[2] ? { line: Number(m[2]) } : {}), ...(m[3] ? { to: Number(m[3]) } : {}) });
|
|
1604
1690
|
}
|
|
1691
|
+
case "effort": {
|
|
1692
|
+
// Speed vs smarts, by name: /effort smarter (or the effort itself: /effort xhigh).
|
|
1693
|
+
const options = EFFORTS.map((e) => EFFORT_LABELS[e].name.toLowerCase()).join(", ");
|
|
1694
|
+
if (!arg)
|
|
1695
|
+
return info(`Now: ${EFFORT_LABELS[settings.effort].name}. ${EFFORT_LABELS[settings.effort].tradeoff} Change it with /effort ${options}.`);
|
|
1696
|
+
const next = effortFrom(arg);
|
|
1697
|
+
if (!next)
|
|
1698
|
+
return info(`No such level: "${arg}". Try one of: ${options}.`, "error");
|
|
1699
|
+
updateSettings({ effort: next });
|
|
1700
|
+
return info(`Now: ${EFFORT_LABELS[next].name}. ${EFFORT_LABELS[next].tradeoff}`);
|
|
1701
|
+
}
|
|
1702
|
+
case "now":
|
|
1703
|
+
if (!queued.length)
|
|
1704
|
+
return info(busy ? "Nothing is queued. Type a message and press Alt+Enter to stop Nou and send it now." : "Nothing is queued.");
|
|
1705
|
+
return sendNow();
|
|
1605
1706
|
case "games":
|
|
1606
1707
|
return toggleGames();
|
|
1607
1708
|
case "cancel":
|
|
@@ -1716,6 +1817,19 @@ ${snippet}
|
|
|
1716
1817
|
return;
|
|
1717
1818
|
case "terminal":
|
|
1718
1819
|
return toggleTerminal();
|
|
1820
|
+
case "redraw":
|
|
1821
|
+
return redraw();
|
|
1822
|
+
case "workspace":
|
|
1823
|
+
case "workspaces":
|
|
1824
|
+
case "ws":
|
|
1825
|
+
return void workspaceCommand(arg);
|
|
1826
|
+
case "fold":
|
|
1827
|
+
case "unfold":
|
|
1828
|
+
if (!openFile)
|
|
1829
|
+
return info("Open a file first: /fold folds every function and block in it, /unfold opens them.", "error");
|
|
1830
|
+
setEditing(false);
|
|
1831
|
+
setFocus("code");
|
|
1832
|
+
return setFoldAll((f) => ({ fold: command === "fold", nonce: (f?.nonce ?? 0) + 1 }));
|
|
1719
1833
|
case "new":
|
|
1720
1834
|
return newChatTab();
|
|
1721
1835
|
case "close":
|
|
@@ -1727,7 +1841,9 @@ ${snippet}
|
|
|
1727
1841
|
return switchChatTab(chatTabs[n - 1]);
|
|
1728
1842
|
}
|
|
1729
1843
|
case "showme":
|
|
1844
|
+
return showPlaces(rest.join(" "));
|
|
1730
1845
|
case "teach":
|
|
1846
|
+
case "teachme":
|
|
1731
1847
|
return showMe(rest.join(" "));
|
|
1732
1848
|
case "resume":
|
|
1733
1849
|
return resumeConversation(rest[0] ?? "");
|
|
@@ -1752,6 +1868,13 @@ ${snippet}
|
|
|
1752
1868
|
}
|
|
1753
1869
|
return;
|
|
1754
1870
|
}
|
|
1871
|
+
// A typo of a command ("/termnial"): one that only changes the screen just runs; others are suggested,
|
|
1872
|
+
// so a slip never clears or undoes anything.
|
|
1873
|
+
const meant = closestCommand(command ?? "", [...SLASH_COMMANDS.map((c) => c.name), ...activePlugins.flatMap(({ plugin }) => plugin.commands?.map((c) => c.name) ?? [])]);
|
|
1874
|
+
if (meant && onlyChangesScreen(meant))
|
|
1875
|
+
return runCommand(`/${meant}${arg ? ` ${arg}` : ""}`);
|
|
1876
|
+
if (meant)
|
|
1877
|
+
return info(`Unknown command /${command}. Did you mean /${meant}${arg ? ` ${arg}` : ""}?`, "error");
|
|
1755
1878
|
return info(`Unknown command /${command}. Type /help.`, "error");
|
|
1756
1879
|
}
|
|
1757
1880
|
}
|
|
@@ -1833,6 +1956,57 @@ ${snippet}
|
|
|
1833
1956
|
setQueued(queued.slice(take));
|
|
1834
1957
|
send(queued.slice(0, take).join("\n\n"));
|
|
1835
1958
|
}, [busy, approval, queued]);
|
|
1959
|
+
// When a reply points at places in the code (Nou's engine with show_places, the others with a ```nou-show
|
|
1960
|
+
// block, which is taken out of the reply), the first one opens highlighted and the rest are listed.
|
|
1961
|
+
useEffect(() => {
|
|
1962
|
+
if (busy || guideAsk.current)
|
|
1963
|
+
return;
|
|
1964
|
+
let stops = tourFound.current;
|
|
1965
|
+
tourFound.current = null;
|
|
1966
|
+
const reply = [...entries].reverse().find((e) => e.kind === "assistant");
|
|
1967
|
+
const found = reply && stopsFromReply(reply.text);
|
|
1968
|
+
if (found) {
|
|
1969
|
+
stops = stops?.length ? stops : found.stops;
|
|
1970
|
+
setEntries((prev) => prev.map((e) => (e === reply ? { ...e, text: found.rest || "Here's where." } : e)));
|
|
1971
|
+
}
|
|
1972
|
+
stops = (stops ?? []).filter((s) => existsSync(s.file));
|
|
1973
|
+
if (!stops.length)
|
|
1974
|
+
return;
|
|
1975
|
+
const list = stops.map((s, i) => `${i + 1}. ${s.file}:${s.line}${s.end > s.line ? `-${s.end}` : ""}${s.note ? ` · ${s.note}` : ""}`).join("\n");
|
|
1976
|
+
info(`📍 ${stops.length === 1 ? "Here it is" : `${stops.length} places`}, open in the code view (click one to jump there${stops.length > 1 ? "; /showme next and /showme back step through them" : ""}):\n${list}`);
|
|
1977
|
+
setTour({ stops, at: 0 });
|
|
1978
|
+
}, [busy]);
|
|
1979
|
+
// Each place: open it in the code view with its lines highlighted.
|
|
1980
|
+
useEffect(() => {
|
|
1981
|
+
const stop = tour?.stops[tour.at];
|
|
1982
|
+
if (!stop)
|
|
1983
|
+
return;
|
|
1984
|
+
openRef({ path: stop.file, line: stop.line, ...(stop.end > stop.line ? { to: stop.end } : {}) });
|
|
1985
|
+
}, [tour?.at, tour?.stops]);
|
|
1986
|
+
/** /showme <question>: explains it and opens the places in the code; /showme next|back|stop steps through them. */
|
|
1987
|
+
const showPlaces = (arg) => {
|
|
1988
|
+
const words = arg.trim();
|
|
1989
|
+
if (/^(stop|end|done)$/.test(words)) {
|
|
1990
|
+
setTour(null);
|
|
1991
|
+
return info(tour ? "Done showing." : "Nothing is being shown.");
|
|
1992
|
+
}
|
|
1993
|
+
if (/^(next|back|prev|previous)$/.test(words)) {
|
|
1994
|
+
if (!tour)
|
|
1995
|
+
return info("Nothing is being shown. Ask with /showme <question>, e.g. /showme where do we check the login?");
|
|
1996
|
+
const at = tour.at + (words === "next" ? 1 : -1);
|
|
1997
|
+
if (at < 0 || at >= tour.stops.length)
|
|
1998
|
+
return info(at < 0 ? "That's the first place." : "That was the last place.");
|
|
1999
|
+
const stop = tour.stops[at];
|
|
2000
|
+
info(`📍 ${at + 1} of ${tour.stops.length}: ${stop.file}:${stop.line}${stop.end > stop.line ? `-${stop.end}` : ""}${stop.note ? ` · ${stop.note}` : ""}`);
|
|
2001
|
+
return setTour({ ...tour, at });
|
|
2002
|
+
}
|
|
2003
|
+
if (!words)
|
|
2004
|
+
return info("Ask how something works or where it happens, and Nou explains it and opens the places in the code, highlighted: /showme where do we check the login? (To be walked through typing a change yourself: /teach.)");
|
|
2005
|
+
if (busy)
|
|
2006
|
+
return info("Wait for the current reply to finish (or press Esc) first.", "error");
|
|
2007
|
+
setTour(null);
|
|
2008
|
+
send(explainPrompt(words, agentRef.current.id === "nou"));
|
|
2009
|
+
};
|
|
1836
2010
|
// When Nou has finished looking, the guide starts at its first step.
|
|
1837
2011
|
useEffect(() => {
|
|
1838
2012
|
if (busy || !guideAsk.current)
|
|
@@ -1858,8 +2032,8 @@ ${snippet}
|
|
|
1858
2032
|
info("It edited files anyway, so those changes were put back: you're the one typing them.");
|
|
1859
2033
|
}
|
|
1860
2034
|
if (!steps?.length)
|
|
1861
|
-
return info("Nou didn't come back with steps to show. Try asking more specifically, e.g. /
|
|
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. /
|
|
2035
|
+
return info("Nou didn't come back with steps to show. Try asking more specifically, e.g. /teach add a --verbose flag to the CLI", "error");
|
|
2036
|
+
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. /teach next skips a step, /teach stop ends it.`);
|
|
1863
2037
|
setGuide({ steps, at: 0, task });
|
|
1864
2038
|
}, [busy]);
|
|
1865
2039
|
// Each step: open its file in the editor where the code goes, and say what to type and why.
|
|
@@ -1895,7 +2069,7 @@ ${snippet}
|
|
|
1895
2069
|
setGuide(null);
|
|
1896
2070
|
}
|
|
1897
2071
|
});
|
|
1898
|
-
/** /
|
|
2072
|
+
/** /teach <what you want>: Nou finds where the change goes and walks you through typing it. */
|
|
1899
2073
|
const showMe = (arg) => {
|
|
1900
2074
|
const words = arg.trim();
|
|
1901
2075
|
if (words === "stop" || words === "end") {
|
|
@@ -1913,8 +2087,8 @@ ${snippet}
|
|
|
1913
2087
|
}
|
|
1914
2088
|
if (!words) {
|
|
1915
2089
|
if (guide)
|
|
1916
|
-
return info(`On step ${guide.at + 1} of ${guide.steps.length}: ${guideStep.file}:${guideStep.line}. /
|
|
1917
|
-
return info("Tell Nou what you want to do and it shows you how, step by step, for you to type: /
|
|
2090
|
+
return info(`On step ${guide.at + 1} of ${guide.steps.length}: ${guideStep.file}:${guideStep.line}. /teach next skips it, /teach stop ends it.`);
|
|
2091
|
+
return info("Tell Nou what you want to do and it shows you how, step by step, for you to type: /teach add a --verbose flag to the CLI");
|
|
1918
2092
|
}
|
|
1919
2093
|
if (busy)
|
|
1920
2094
|
return info("Wait for the current reply to finish (or press Esc) first.", "error");
|
|
@@ -1928,10 +2102,21 @@ ${snippet}
|
|
|
1928
2102
|
if (busy)
|
|
1929
2103
|
stopped.current = false;
|
|
1930
2104
|
}, [busy]);
|
|
1931
|
-
/**
|
|
1932
|
-
const
|
|
2105
|
+
/** Stops Nou and sends what's queued right away (a click on the queue, or /now). */
|
|
2106
|
+
const sendNow = () => {
|
|
2107
|
+
if (!queued.length)
|
|
2108
|
+
return;
|
|
2109
|
+
stop();
|
|
2110
|
+
stopped.current = false; // what's queued is meant to go
|
|
2111
|
+
};
|
|
2112
|
+
/**
|
|
2113
|
+
* From the chat: while Nou works, a message waits its turn, or (`now`: Alt+Enter) stops Nou and goes right
|
|
2114
|
+
* away. The "Send while Nou works" setting swaps the two, so Enter sends now and Alt+Enter queues.
|
|
2115
|
+
*/
|
|
2116
|
+
const submit = (text, alt = false) => {
|
|
1933
2117
|
if (!busy || (text.startsWith("/") && runsWhileBusy(text)))
|
|
1934
2118
|
return send(text);
|
|
2119
|
+
const now = settings.sendNowWhileBusy ? !alt : alt;
|
|
1935
2120
|
if (now) {
|
|
1936
2121
|
stop();
|
|
1937
2122
|
stopped.current = false; // this one is meant to go
|
|
@@ -1941,6 +2126,11 @@ ${snippet}
|
|
|
1941
2126
|
setQueued((q) => [...q, text]);
|
|
1942
2127
|
};
|
|
1943
2128
|
const send = (text) => {
|
|
2129
|
+
// Right after /resume lists them, a number picks a conversation instead of being sent to the agent.
|
|
2130
|
+
const picking = pickingResume.current;
|
|
2131
|
+
pickingResume.current = false;
|
|
2132
|
+
if (picking && /^\s*\d+\s*$/.test(text))
|
|
2133
|
+
return void resumeConversation(text.trim());
|
|
1944
2134
|
if (text.startsWith("/")) {
|
|
1945
2135
|
void runCommand(text);
|
|
1946
2136
|
return;
|
|
@@ -1984,7 +2174,7 @@ ${snippet}
|
|
|
1984
2174
|
const onTreeHover = useEvent(() => { });
|
|
1985
2175
|
const onJump = useEvent((file, line) => openAt(file, line));
|
|
1986
2176
|
const onCloseProblems = useEvent(() => setCodePanel("code"));
|
|
1987
|
-
const onViewport = useEvent((top, bottom) => setViewport((v) => (v?.top === top && v.bottom === bottom ? v : { top, bottom })));
|
|
2177
|
+
const onViewport = useEvent((top, bottom, lines) => setViewport((v) => (v?.top === top && v.bottom === bottom && v.lines?.join() === lines?.join() ? v : { top, bottom, ...(lines ? { lines } : {}) })));
|
|
1988
2178
|
const onSend = useEvent(submit);
|
|
1989
2179
|
const onChatMenu = useEvent((open) => {
|
|
1990
2180
|
chatMenuOpen.current = open;
|
|
@@ -2080,6 +2270,56 @@ ${snippet}
|
|
|
2080
2270
|
updateSettings({ mouse: !settings.mouse });
|
|
2081
2271
|
info(settings.mouse ? "Mouse off: drag to select text and copy it as usual in your terminal. m turns it back on." : "Mouse on: wheel scrolls, click focuses, drag borders to resize.");
|
|
2082
2272
|
};
|
|
2273
|
+
/** /redraw, Ctrl+L: draws the whole screen again, in case anything else wrote to the terminal and shifted it. */
|
|
2274
|
+
function redraw() {
|
|
2275
|
+
forgetScreen();
|
|
2276
|
+
setRepaint((n) => n + 1);
|
|
2277
|
+
}
|
|
2278
|
+
/** /workspace: list them, `new [folder]` opens one, a number switches. */
|
|
2279
|
+
async function workspaceCommand(arg) {
|
|
2280
|
+
const [sub, ...more] = arg.split(/\s+/);
|
|
2281
|
+
const tabs = workspaceTabs();
|
|
2282
|
+
if (!sub) {
|
|
2283
|
+
if (!isHosted())
|
|
2284
|
+
return info(`One workspace: ${path.basename(process.cwd())}. /workspace new <folder> opens another in this window, a tab each in the header (click + there too).`);
|
|
2285
|
+
return info(`Workspaces: ${tabs.map((t, i) => `${i + 1}. ${t.name}${t.busy ? " (working)" : ""}${t.active ? " ← here" : ""}`).join(" ")}. /workspace <n> switches, /workspace new <folder> opens one, /quit closes this one.`);
|
|
2286
|
+
}
|
|
2287
|
+
if (/^\d+$/.test(sub)) {
|
|
2288
|
+
const n = Number(sub);
|
|
2289
|
+
if (!isHosted() || !tabs[n - 1])
|
|
2290
|
+
return info(`There's no workspace ${n}. /workspace lists them.`, "error");
|
|
2291
|
+
return void sendToHost({ type: "switch", index: n - 1 });
|
|
2292
|
+
}
|
|
2293
|
+
if (sub !== "new")
|
|
2294
|
+
return info("Try /workspace new <folder>, /workspace <n> or /workspace.", "error");
|
|
2295
|
+
const wanted = more.join(" ");
|
|
2296
|
+
if (isHosted())
|
|
2297
|
+
return void sendToHost({ type: "new", folder: wanted });
|
|
2298
|
+
const folder = workspaceFolder(wanted, process.cwd());
|
|
2299
|
+
if (!folder)
|
|
2300
|
+
return info(`No such folder: ${wanted}`, "error");
|
|
2301
|
+
if (busy)
|
|
2302
|
+
return info("Wait for the current reply to finish (or press Esc) first: Nou reopens as a workspaces window.", "error");
|
|
2303
|
+
// The pseudo-terminals workspaces run in come from a small native package, installed once like an engine.
|
|
2304
|
+
const pty = { id: "nou", label: "workspaces", description: "", approvals: "before", model: () => "", package: PTY_PACKAGE };
|
|
2305
|
+
if (!isInstalled(pty)) {
|
|
2306
|
+
info(`Workspaces need ${PTY_PACKAGE} (pseudo-terminals, prebuilt for macOS, Linux and Windows). Installing it once into ~/.nou/engines...`);
|
|
2307
|
+
setBusy("Installing workspaces support (npm)...");
|
|
2308
|
+
try {
|
|
2309
|
+
await installEngine(pty, (line) => setBusy(`Installing workspaces support (npm): ${line.slice(0, 70)}`));
|
|
2310
|
+
}
|
|
2311
|
+
catch (error) {
|
|
2312
|
+
return info(`Couldn't install ${PTY_PACKAGE}: ${error.message}`, "error");
|
|
2313
|
+
}
|
|
2314
|
+
finally {
|
|
2315
|
+
setBusy(null);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
if (draft.current && settings.autoSave !== "off")
|
|
2319
|
+
saveFromEditor(draft.current.text, draft.current.path);
|
|
2320
|
+
reopenAsWorkspaces([process.cwd(), folder]);
|
|
2321
|
+
exit();
|
|
2322
|
+
}
|
|
2083
2323
|
/** Opens the terminal panel (starting its shell the first time) and gives it the keyboard, or closes it. */
|
|
2084
2324
|
function toggleTerminal(open = !termOpen) {
|
|
2085
2325
|
shellRef.current ??= new Shell(() => setTermVersion((v) => v + 1));
|
|
@@ -2113,6 +2353,7 @@ ${snippet}
|
|
|
2113
2353
|
setFocus(beforeGame.current);
|
|
2114
2354
|
};
|
|
2115
2355
|
// Plain functions, not hooks: this runs after the settings screen's early return.
|
|
2356
|
+
const onGameScore = (game, score) => updateSettings({ gameBest: { ...(settings.gameBest ?? {}), [game]: score } });
|
|
2116
2357
|
const leaveGames = () => setFocus(beforeGame.current === "game" ? "chat" : beforeGame.current);
|
|
2117
2358
|
const gamesSettings = () => {
|
|
2118
2359
|
setFocus(beforeGame.current === "game" ? "chat" : beforeGame.current);
|
|
@@ -2145,7 +2386,7 @@ ${snippet}
|
|
|
2145
2386
|
{ title: "MCP servers: tools from other programs", keys: "/mcp", run: () => void runCommand("/mcp") },
|
|
2146
2387
|
{ title: "Browser control (Playwright MCP)", keys: "/mcp add browser", run: () => void runCommand("/mcp add browser") },
|
|
2147
2388
|
{ title: "Import MCP servers from Claude Code, Cursor, Codex...", keys: "/mcp import", run: () => void runCommand("/mcp import") },
|
|
2148
|
-
{ title: gamesOpen ? "Close the Games panel" : "Games: Pong, Snake, Breakout", keys: "/games", run: () => toggleGames() },
|
|
2389
|
+
{ title: gamesOpen ? "Close the Games panel" : "Games: Pong, Snake, Breakout, Dodge, Hop, Pairs", keys: "/games", run: () => toggleGames() },
|
|
2149
2390
|
{ title: "Next tab", keys: "]", run: () => cycleTab(1) },
|
|
2150
2391
|
{ title: "Previous tab", keys: "[", run: () => cycleTab(-1) },
|
|
2151
2392
|
{ title: "Close the tab", keys: "x", run: () => closeTab() },
|
|
@@ -2252,9 +2493,9 @@ ${snippet}
|
|
|
2252
2493
|
}
|
|
2253
2494
|
const modelLabel = agentRef.current.id === "nou" ? describe(choice) : `${engineInfo(agentRef.current.id).label} · ${engineInfo(agentRef.current.id).model(choice)}`;
|
|
2254
2495
|
clickable.current = { status: zones, modelFrom: columns - (" ⌂ LOCAL ".length + ` ${modelLabel} `.length + 1) };
|
|
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:
|
|
2496
|
+
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, tabs: tabsOfWorkspaces }), _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, foldClick: foldClick, foldAll: foldAll })), _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, sendNowFirst: settings.sendNowWhileBusy, onSendNow: sendNow, 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: C.onBadge, 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
|
|
2256
2497
|
? _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)` : ""] })
|
|
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) => {
|
|
2498
|
+
: 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, onClose: () => toggleTerminal(false), onResize: resizeTerminal }) })), layout.current.game && (_jsx(Box, { position: "absolute", marginLeft: layout.current.game.x, marginTop: layout.current.game.y, children: _jsx(GameGuard, { children: _jsx(GamePanel, { width: layout.current.game.width, height: layout.current.game.height, focused: focus === "game", moving: !!gameDrag, installed: settings.games, best: settings.gameBest ?? {}, onScore: onGameScore, onLeave: leaveGames, onOpenSettings: gamesSettings }) }) })), quickOpen !== null && (_jsx(QuickOpen, { width: columns, top: 3, files: allFiles, recent: recent, actions: resumePicker ?? paletteActions, initialQuery: quickOpen, ...(resumePicker ? { placeholder: "Pick a conversation to resume (type to filter)" } : {}), onPick: (file) => {
|
|
2258
2499
|
setQuickOpen(null);
|
|
2259
2500
|
clearFollow();
|
|
2260
2501
|
open(file);
|
|
@@ -2262,6 +2503,10 @@ ${snippet}
|
|
|
2262
2503
|
setFocus("code");
|
|
2263
2504
|
}, onRun: (action) => {
|
|
2264
2505
|
setQuickOpen(null);
|
|
2506
|
+
setResumePicker(null);
|
|
2265
2507
|
action.run();
|
|
2266
|
-
}, onClose: () =>
|
|
2508
|
+
}, onClose: () => {
|
|
2509
|
+
setQuickOpen(null);
|
|
2510
|
+
setResumePicker(null);
|
|
2511
|
+
} }))] }));
|
|
2267
2512
|
}
|