nou-cli 0.5.6 → 0.5.11
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 +14 -7
- package/dist/src/agent/index.js +1 -0
- package/dist/src/cli.js +36 -0
- package/dist/src/config/index.js +19 -0
- package/dist/src/conversations/external.js +35 -13
- package/dist/src/guide/index.js +75 -0
- package/dist/src/ui/App.js +282 -39
- package/dist/src/ui/Chat.js +60 -11
- package/dist/src/ui/CodeView.js +7 -6
- package/dist/src/ui/Editor.js +5 -4
- package/dist/src/ui/FileTree.js +6 -5
- package/dist/src/ui/GamePanel.js +1 -1
- package/dist/src/ui/Header.js +39 -3
- package/dist/src/ui/Pane.js +2 -2
- package/dist/src/ui/ProblemsView.js +3 -3
- package/dist/src/ui/QuickOpen.js +8 -3
- package/dist/src/ui/Settings.js +14 -7
- package/dist/src/ui/Terminal.js +7 -1
- package/dist/src/ui/background.js +58 -0
- package/dist/src/ui/commands.js +46 -6
- package/dist/src/ui/keys.js +8 -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";
|
|
@@ -26,21 +26,24 @@ import { changedLines, lineCounts, loadFallbackHighlighter, splitLines } from ".
|
|
|
26
26
|
import { findDefinition, wordAt } from "./definition.js";
|
|
27
27
|
import { ProblemsView } from "./ProblemsView.js";
|
|
28
28
|
import { useFocusReporting, useTerminalFocused } from "./focus.js";
|
|
29
|
-
import { useMouse } from "./mouse.js";
|
|
30
|
-
import {
|
|
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";
|
|
33
|
+
import { FIND_KEY, isFind } from "./keys.js";
|
|
31
34
|
import { FileTree } from "./FileTree.js";
|
|
32
35
|
import { CodeView } from "./CodeView.js";
|
|
33
36
|
import { Editor } from "./Editor.js";
|
|
34
|
-
import { SLASH_COMMANDS, runsWhileBusy } from "./commands.js";
|
|
37
|
+
import { SLASH_COMMANDS, closestCommand, onlyChangesScreen, runsWhileBusy } from "./commands.js";
|
|
35
38
|
import { WHEEL_STEP } from "./scroll.js";
|
|
36
39
|
import { Chat, chatTabZones } from "./Chat.js";
|
|
37
40
|
import { copyToClipboard, openInBrowser } from "./clipboard.js";
|
|
38
|
-
import { Header } from "./Header.js";
|
|
41
|
+
import { Header, headerZones } from "./Header.js";
|
|
39
42
|
import { Tabs, layoutTabs } from "./Tabs.js";
|
|
40
43
|
import { GAME_PANEL, GameGuard, GamePanel } from "./GamePanel.js";
|
|
41
44
|
import { Terminal } from "./Terminal.js";
|
|
42
45
|
import { forgetScreen } from "./redraw.js";
|
|
43
|
-
import { showMePrompt, showStepsTool, stepsFromReply } from "../guide/index.js";
|
|
46
|
+
import { SHOW_PLACES_NOTE, explainPrompt, showMePrompt, showPlacesTool, showStepsTool, stepsFromReply, stopsFromReply } from "../guide/index.js";
|
|
44
47
|
import { Shell } from "../terminal/index.js";
|
|
45
48
|
import { QuickOpen, listAllFiles, score } from "./QuickOpen.js";
|
|
46
49
|
import { Settings } from "./Settings.js";
|
|
@@ -81,16 +84,16 @@ Keys:
|
|
|
81
84
|
m mouse on/off (off: select text to copy; Shift+drag also works in most terminals)
|
|
82
85
|
/ start a command from any pane
|
|
83
86
|
e edit open file (or double-click in the code) v diff (git / Nou / session) f follow mode
|
|
84
|
-
|
|
87
|
+
${FIND_KEY} find in the open file (Enter/↓ next, ↑ previous)
|
|
85
88
|
b back to the previous file (Ctrl+P lists recent files first) [ ] previous/next tab · x close tab
|
|
86
89
|
Ctrl+click, Option+click or right-click a name: go to its definition (an import's path opens the file)
|
|
87
90
|
p problems u undo Nou's edit s settings
|
|
88
91
|
c (in files) show only changed files Ctrl+C twice quit
|
|
89
|
-
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
|
|
92
|
+
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
|
|
90
93
|
In the editor:
|
|
91
94
|
Shift+arrows / mouse drag select · Ctrl+A all · Ctrl+C/X/V copy/cut/paste · Esc clear selection or leave
|
|
92
95
|
Ctrl+←/→ word · Ctrl+Backspace or Ctrl+W delete word · Ctrl+Home/End file start/end
|
|
93
|
-
|
|
96
|
+
${FIND_KEY} find · Ctrl+L go to line · Ctrl+/ comment · Ctrl+D duplicate line · Alt+↑/↓ move line
|
|
94
97
|
Tab/Shift+Tab indent/outdent · Ctrl+S save · Ctrl+Z/Y undo/redo · Ctrl+K info · Ctrl+G go to definition`;
|
|
95
98
|
/** A callback that keeps one identity across renders but always runs the latest code, so memoized panes don't re-render for it. */
|
|
96
99
|
function useEvent(fn) {
|
|
@@ -110,13 +113,17 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
110
113
|
const { exit } = useApp();
|
|
111
114
|
const { columns, rows } = useWindowSize();
|
|
112
115
|
const [settings, setSettings] = useState(loadSettings);
|
|
113
|
-
|
|
116
|
+
// Before anything below highlights code. One Dark's light text is unreadable on a light terminal: there the
|
|
117
|
+
// default switches to GitHub Light (any other theme you picked stays).
|
|
118
|
+
setCodeTheme(palette() === "light" && settings.codeTheme === "one-dark" ? "github-light" : settings.codeTheme);
|
|
114
119
|
const [view, setView] = useState("main");
|
|
115
120
|
const [connectTo, setConnectTo] = useState();
|
|
116
121
|
/** Why settings were opened for you (a request failed on the provider's setup). */
|
|
117
122
|
const [setupReason, setSetupReason] = useState(null);
|
|
118
123
|
/** The file finder / command palette: closed (null), or open with this query (">" for commands). */
|
|
119
124
|
const [quickOpen, setQuickOpen] = useState(null);
|
|
125
|
+
/** /resume's picker: the conversations, in the palette's place while it's open. */
|
|
126
|
+
const [resumePicker, setResumePicker] = useState(null);
|
|
120
127
|
const [focus, setFocus] = useState("chat");
|
|
121
128
|
/** The Games panel: whether it's open, the pane it's being dragged over, and the pane to go back to. */
|
|
122
129
|
const [gamesOpen, setGamesOpen] = useState(false);
|
|
@@ -136,7 +143,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
136
143
|
/** The tab strip, kept while nothing in it changes: a new one would redraw the whole code view on every scroll. */
|
|
137
144
|
const tabStripCache = useRef(null);
|
|
138
145
|
/**
|
|
139
|
-
* "
|
|
146
|
+
* "Teach me" (/teach): the steps Nou found, and the one you're on. While Nou looks for them,
|
|
140
147
|
* `guideAsk` holds what you asked (and its edits are turned down: you're doing the typing).
|
|
141
148
|
*/
|
|
142
149
|
const [guide, setGuide] = useState(null);
|
|
@@ -146,6 +153,20 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
146
153
|
/** /resume just listed the conversations: the next message, if it's a number, picks one. */
|
|
147
154
|
const pickingResume = useRef(false);
|
|
148
155
|
const guideFound = useRef(null);
|
|
156
|
+
/**
|
|
157
|
+
* "Show me" (/showme, or any explanation that points at code): the places the agent pointed at, and the
|
|
158
|
+
* one open in the code view. `tourFound` holds what Nou's engine passed to show_places during a turn.
|
|
159
|
+
*/
|
|
160
|
+
const [tour, setTour] = useState(null);
|
|
161
|
+
const tourFound = useRef(null);
|
|
162
|
+
/** The files on /showme's tour and which of its places are in each, for the file tree. */
|
|
163
|
+
const tourFiles = useMemo(() => {
|
|
164
|
+
if (!tour)
|
|
165
|
+
return null;
|
|
166
|
+
const map = new Map();
|
|
167
|
+
tour.stops.forEach((s, i) => map.set(s.file, [...(map.get(s.file) ?? []), i + 1]));
|
|
168
|
+
return map;
|
|
169
|
+
}, [tour]);
|
|
149
170
|
const shellRef = useRef(null);
|
|
150
171
|
const beforeTerm = useRef("code");
|
|
151
172
|
const [follow, setFollow] = useState(settings.followMode);
|
|
@@ -243,7 +264,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
243
264
|
const [findRequest, setFindRequest] = useState(0);
|
|
244
265
|
const startFind = () => {
|
|
245
266
|
if (!openFile)
|
|
246
|
-
return info(
|
|
267
|
+
return info(`Open a file first (Ctrl+P), then ${FIND_KEY} searches it.`);
|
|
247
268
|
setDiffMode("off");
|
|
248
269
|
setEditing(false);
|
|
249
270
|
setCodePanel("code");
|
|
@@ -258,6 +279,12 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
258
279
|
const layout = useRef({ treeWidth: 0, codeWidth: 0, paneHeight: 0, heights: { tree: 0, code: 0, chat: 0 }, game: null, terminal: null });
|
|
259
280
|
/** Pane widths while a border is dragged (saved on release); the saved ones otherwise. */
|
|
260
281
|
const [paneWidths, setPaneWidths] = useState(settings.paneWidths);
|
|
282
|
+
/**
|
|
283
|
+
* How far the code panel is zoomed (+ and - in the code, /zoom in|out): each step makes it a few columns
|
|
284
|
+
* wider (taken from the files and chat) or narrower (given to them). 0 is the normal layout.
|
|
285
|
+
*/
|
|
286
|
+
const [codeZoom, setCodeZoom] = useState(0);
|
|
287
|
+
const zoomCode = (by) => setCodeZoom((z) => (by === 0 ? 0 : Math.max(-6, Math.min(8, z + by))));
|
|
261
288
|
const paneWidthsRef = useRef(paneWidths);
|
|
262
289
|
paneWidthsRef.current = paneWidths;
|
|
263
290
|
/** What a mouse drag is moving: a pane border, a pane's scrollbar, or the editor's selection. */
|
|
@@ -545,7 +572,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
545
572
|
useEffect(() => {
|
|
546
573
|
const { extras, extraTools } = harness({ diagnostics, plugins: activePlugins.map((p) => p.plugin), git: settings.gitContext && repo });
|
|
547
574
|
// Other engines read AGENTS.md / CLAUDE.md themselves: they get only the NOU.md rules on top.
|
|
548
|
-
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 };
|
|
575
|
+
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 };
|
|
549
576
|
agentRef.current.configure(engineConfig.current);
|
|
550
577
|
}, [settings.gitContext, settings.effort, activePlugins, mcpVersion]);
|
|
551
578
|
// Pick up changes made outside Nou (other editors, git commands). Not while Nou's window is in the
|
|
@@ -581,6 +608,24 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
581
608
|
if (openFile && diffMode === "off" && !editing)
|
|
582
609
|
setContent(readText(openFile));
|
|
583
610
|
}, [busy]);
|
|
611
|
+
/**
|
|
612
|
+
* Workspaces: when this Nou is one tab of several, the host says when it's on screen again (redraw all of
|
|
613
|
+
* it, and turn the mouse and focus reports back on, which another workspace may have turned off on its way
|
|
614
|
+
* out), and hears when it's busy, for the tab's ● .
|
|
615
|
+
*/
|
|
616
|
+
const tabsOfWorkspaces = useWorkspaceTabs();
|
|
617
|
+
useEffect(() => {
|
|
618
|
+
connectToHost({
|
|
619
|
+
redraw: () => {
|
|
620
|
+
process.stdout.write(`\x1b[?1049h${settings.mouse ? MOUSE_ON : ""}\x1b[?1004h`);
|
|
621
|
+
redraw();
|
|
622
|
+
},
|
|
623
|
+
error: (text) => info(text, "error"),
|
|
624
|
+
});
|
|
625
|
+
}, []);
|
|
626
|
+
useEffect(() => {
|
|
627
|
+
sendToHost({ type: "state", busy: !!busy });
|
|
628
|
+
}, [!!busy]);
|
|
584
629
|
const updateSettings = (patch) => setSettings(saveSettings(patch));
|
|
585
630
|
const switchModel = (next) => {
|
|
586
631
|
const problem = next.provider.unavailable();
|
|
@@ -642,19 +687,31 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
642
687
|
const list = resumable();
|
|
643
688
|
if (!list.length)
|
|
644
689
|
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.");
|
|
690
|
+
const where = (c) => ("file" in c ? `${engineInfo(c.engine).label} CLI` : engineInfo(isEngineId(c.engine) ? c.engine : "nou").label);
|
|
645
691
|
if (!arg) {
|
|
646
|
-
const rows = list.slice(0, 10).map((c, i) => ` ${String(i + 1).padStart(2)}. ${c.title} (${c.messages} message${c.messages === 1 ? "" : "s"} · ${
|
|
692
|
+
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)})`);
|
|
647
693
|
pickingResume.current = true; // a bare number, typed next, picks one
|
|
648
|
-
|
|
694
|
+
info(`Earlier conversations here (also in the picker: ↑↓ and Enter, or type a number here):\n${rows.join("\n")}`);
|
|
695
|
+
// A picker over the screen: ↑↓ and Enter, or type to filter by title.
|
|
696
|
+
setResumePicker(list.map((c, i) => ({ title: `${i + 1}. ${c.title}`, keys: `${where(c)} · ${ago(c.updated)}`, run: () => void resumePicked(c) })));
|
|
697
|
+
setQuickOpen(">");
|
|
698
|
+
return;
|
|
649
699
|
}
|
|
650
700
|
const pick = arg === "last" ? list[0] : list[Number(arg) - 1] ?? list.find((c) => c.id === arg);
|
|
651
701
|
if (!pick)
|
|
652
702
|
return info(`There's no conversation ${arg}: /resume lists them.`, "error");
|
|
703
|
+
return resumePicked(pick);
|
|
704
|
+
};
|
|
705
|
+
/** Picks a conversation back up: Nou's own, or a Claude Code or Codex CLI session. */
|
|
706
|
+
const resumePicked = async (pick) => {
|
|
707
|
+
pickingResume.current = false;
|
|
708
|
+
if (busy)
|
|
709
|
+
return info("Wait for the current reply to finish (or press Esc) before resuming another conversation.", "error");
|
|
653
710
|
if ("file" in pick)
|
|
654
711
|
return resumeExternal(pick);
|
|
655
712
|
const saved = loadConversation(pick.id);
|
|
656
713
|
if (!saved)
|
|
657
|
-
return info(`Couldn't read conversation ${
|
|
714
|
+
return info(`Couldn't read conversation "${pick.title}".`, "error");
|
|
658
715
|
const engine = engineFor(isEngineId(saved.engine) ? saved.engine : null);
|
|
659
716
|
const restored = saved.engine === engine.id && !!saved.state && !!engine.restore?.(saved.state);
|
|
660
717
|
if (!restored) {
|
|
@@ -1020,6 +1077,14 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1020
1077
|
}
|
|
1021
1078
|
if (event.kind === "click" && !approval && event.y === 0 && event.x >= clickable.current.modelFrom)
|
|
1022
1079
|
return setView("settings");
|
|
1080
|
+
// Workspace tabs in the header: click one to switch to it, + to open another.
|
|
1081
|
+
if (event.kind === "click" && event.y === 0) {
|
|
1082
|
+
const zone = headerZones(workspaceTabs()).find((z) => event.x >= z.from && event.x < z.to);
|
|
1083
|
+
if (zone?.tab === "new")
|
|
1084
|
+
return typeCommand("/workspace new ");
|
|
1085
|
+
if (typeof zone?.tab === "number")
|
|
1086
|
+
return void sendToHost({ type: "switch", index: zone.tab });
|
|
1087
|
+
}
|
|
1023
1088
|
if (event.kind === "click" && event.y === paneHeight + 1)
|
|
1024
1089
|
return clickable.current.status.find((z) => event.x >= z.from && event.x < z.to)?.run();
|
|
1025
1090
|
if (event.y < 1 || event.y > paneHeight)
|
|
@@ -1097,8 +1162,14 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1097
1162
|
}
|
|
1098
1163
|
// Two clicks in the same spot within half a second are a double-click.
|
|
1099
1164
|
const last = lastClick.current;
|
|
1100
|
-
const
|
|
1101
|
-
|
|
1165
|
+
const again = !!last && Date.now() - last.time < DOUBLE_CLICK_MS && Math.abs(last.x - event.x) <= 1 && last.y === event.y;
|
|
1166
|
+
const count = again ? last.count + 1 : 1;
|
|
1167
|
+
const double = count === 2;
|
|
1168
|
+
// The chat counts a third click too (the whole paragraph); elsewhere a double-click starts over.
|
|
1169
|
+
lastClick.current = double && pane !== "chat" ? null : count >= 3 ? null : { x: event.x, y: event.y, time: Date.now(), count };
|
|
1170
|
+
// In the chat: a double-click selects and copies the word (or path, or link), a triple-click the paragraph.
|
|
1171
|
+
if (pane === "chat" && event.y >= 3 && count >= 2)
|
|
1172
|
+
chatAt(event.x, event.y, count === 2 ? "word" : "block");
|
|
1102
1173
|
if (pane === "code" && event.y >= 3 && codePanel === "code") {
|
|
1103
1174
|
// As the code view counts them (a last empty line after the final newline isn't shown).
|
|
1104
1175
|
const digits = String(splitLines(content).length).length;
|
|
@@ -1423,6 +1494,16 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1423
1494
|
return; // so does the find bar
|
|
1424
1495
|
if (focus === "game" || focus === "terminal")
|
|
1425
1496
|
return; // and the Games panel and the terminal
|
|
1497
|
+
// Cmd+F (Mac) or Ctrl+F from the files or the chat: find in the open file (the code view takes it itself
|
|
1498
|
+
// when it has the keyboard). Other Cmd+letters aren't Nou's: a plain letter is, so Cmd+F never also
|
|
1499
|
+
// toggles follow mode (f).
|
|
1500
|
+
if (isFind(input, key)) {
|
|
1501
|
+
if (focus !== "code")
|
|
1502
|
+
startFind();
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
if (key.super)
|
|
1506
|
+
return;
|
|
1426
1507
|
if (key.tab && key.shift)
|
|
1427
1508
|
return cycleAutoMode();
|
|
1428
1509
|
if (key.tab && focus === "chat" && chatMenuOpen.current)
|
|
@@ -1470,9 +1551,13 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1470
1551
|
updateSettings({ paneWidths: moveBorder(focus === "tree" ? "tree" : "chat", (focus === "tree" ? treeWidth - 1 : treeWidth + codeWidth) + step) });
|
|
1471
1552
|
}
|
|
1472
1553
|
else if (input === "=") {
|
|
1554
|
+
setCodeZoom(0);
|
|
1473
1555
|
setPaneWidths(null);
|
|
1474
1556
|
updateSettings({ paneWidths: null });
|
|
1475
1557
|
}
|
|
1558
|
+
else if ((input === "+" || input === "-") && focus === "code") {
|
|
1559
|
+
zoomCode(input === "+" ? 1 : -1);
|
|
1560
|
+
}
|
|
1476
1561
|
else if (input === "v")
|
|
1477
1562
|
cycleDiff();
|
|
1478
1563
|
else if ((input === "]" || input === "[") && diffMode === "session")
|
|
@@ -1563,7 +1648,7 @@ ${snippet}
|
|
|
1563
1648
|
if (action)
|
|
1564
1649
|
return info("Usage: /mcp, /mcp add <name> <command|url>, /mcp add browser, /mcp remove <name>, /mcp import, /mcp reconnect", "error");
|
|
1565
1650
|
const list = mcpRef.current.status();
|
|
1566
|
-
const mark = { ready: "●", connecting: "◌", failed: "
|
|
1651
|
+
const mark = { ready: "●", connecting: "◌", failed: "✗", off: "○" };
|
|
1567
1652
|
const lines = list.map((x) => ` ${mark[x.state]} ${x.name.padEnd(16)} ${x.state === "ready" ? `${x.tools} tools` : x.state}${x.scope === "project" ? " · this project" : ""}${x.error ? ` · ${x.error}` : ""}`);
|
|
1568
1653
|
const others = findOtherMcpServers().filter((f) => !list.some((x) => x.name === f.name));
|
|
1569
1654
|
return info([
|
|
@@ -1600,7 +1685,7 @@ ${snippet}
|
|
|
1600
1685
|
const mark = { on: "●", off: "○", changed: "⚠" };
|
|
1601
1686
|
const lines = allPlugins.map((p) => p.scope === "starter"
|
|
1602
1687
|
? ` + ${p.id.padEnd(16)} ${p.plugin?.description ?? ""}`
|
|
1603
|
-
: ` ${mark[trustState(p, settings)]} ${p.id.padEnd(16)} ${p.scope}${p.error ? ` ·
|
|
1688
|
+
: ` ${mark[trustState(p, settings)]} ${p.id.padEnd(16)} ${p.scope}${p.error ? ` · ✗ ${p.error}` : ""}`);
|
|
1604
1689
|
return info(`Plugins (● on, ○ off, + starter you can add with /plugins add <name>):\n${lines.join("\n")}\nWrite your own: /plugins new <name>`);
|
|
1605
1690
|
};
|
|
1606
1691
|
const runCommand = async (input) => {
|
|
@@ -1632,6 +1717,21 @@ ${snippet}
|
|
|
1632
1717
|
return info(`No file matches "${wanted}". To find where something is in the code, ask in the chat.`, "error");
|
|
1633
1718
|
return openRef({ path: file, ...(m[2] ? { line: Number(m[2]) } : {}), ...(m[3] ? { to: Number(m[3]) } : {}) });
|
|
1634
1719
|
}
|
|
1720
|
+
case "effort": {
|
|
1721
|
+
// Speed vs smarts, by name: /effort smarter (or the effort itself: /effort xhigh).
|
|
1722
|
+
const options = EFFORTS.map((e) => EFFORT_LABELS[e].name.toLowerCase()).join(", ");
|
|
1723
|
+
if (!arg)
|
|
1724
|
+
return info(`Now: ${EFFORT_LABELS[settings.effort].name}. ${EFFORT_LABELS[settings.effort].tradeoff} Change it with /effort ${options}.`);
|
|
1725
|
+
const next = effortFrom(arg);
|
|
1726
|
+
if (!next)
|
|
1727
|
+
return info(`No such level: "${arg}". Try one of: ${options}.`, "error");
|
|
1728
|
+
updateSettings({ effort: next });
|
|
1729
|
+
return info(`Now: ${EFFORT_LABELS[next].name}. ${EFFORT_LABELS[next].tradeoff}`);
|
|
1730
|
+
}
|
|
1731
|
+
case "now":
|
|
1732
|
+
if (!queued.length)
|
|
1733
|
+
return info(busy ? "Nothing is queued. Type a message and press Alt+Enter to stop Nou and send it now." : "Nothing is queued.");
|
|
1734
|
+
return sendNow();
|
|
1635
1735
|
case "games":
|
|
1636
1736
|
return toggleGames();
|
|
1637
1737
|
case "cancel":
|
|
@@ -1748,6 +1848,18 @@ ${snippet}
|
|
|
1748
1848
|
return toggleTerminal();
|
|
1749
1849
|
case "redraw":
|
|
1750
1850
|
return redraw();
|
|
1851
|
+
case "zoom":
|
|
1852
|
+
if (/^(in|\+|bigger)$/.test(arg))
|
|
1853
|
+
return zoomCode(1);
|
|
1854
|
+
if (/^(out|-|smaller)$/.test(arg))
|
|
1855
|
+
return zoomCode(-1);
|
|
1856
|
+
if (/^(reset|0|off)$/.test(arg))
|
|
1857
|
+
return zoomCode(0);
|
|
1858
|
+
return info("/zoom in makes the code panel bigger, /zoom out smaller, /zoom reset puts it back (or + and - with the code focused, = to reset). The files and chat give or take the room. For bigger text, use your terminal's own zoom (Cmd/Ctrl and +).");
|
|
1859
|
+
case "workspace":
|
|
1860
|
+
case "workspaces":
|
|
1861
|
+
case "ws":
|
|
1862
|
+
return void workspaceCommand(arg);
|
|
1751
1863
|
case "fold":
|
|
1752
1864
|
case "unfold":
|
|
1753
1865
|
if (!openFile)
|
|
@@ -1766,7 +1878,9 @@ ${snippet}
|
|
|
1766
1878
|
return switchChatTab(chatTabs[n - 1]);
|
|
1767
1879
|
}
|
|
1768
1880
|
case "showme":
|
|
1881
|
+
return showPlaces(rest.join(" "));
|
|
1769
1882
|
case "teach":
|
|
1883
|
+
case "teachme":
|
|
1770
1884
|
return showMe(rest.join(" "));
|
|
1771
1885
|
case "resume":
|
|
1772
1886
|
return resumeConversation(rest[0] ?? "");
|
|
@@ -1791,6 +1905,13 @@ ${snippet}
|
|
|
1791
1905
|
}
|
|
1792
1906
|
return;
|
|
1793
1907
|
}
|
|
1908
|
+
// A typo of a command ("/termnial"): one that only changes the screen just runs; others are suggested,
|
|
1909
|
+
// so a slip never clears or undoes anything.
|
|
1910
|
+
const meant = closestCommand(command ?? "", [...SLASH_COMMANDS.map((c) => c.name), ...activePlugins.flatMap(({ plugin }) => plugin.commands?.map((c) => c.name) ?? [])]);
|
|
1911
|
+
if (meant && onlyChangesScreen(meant))
|
|
1912
|
+
return runCommand(`/${meant}${arg ? ` ${arg}` : ""}`);
|
|
1913
|
+
if (meant)
|
|
1914
|
+
return info(`Unknown command /${command}. Did you mean /${meant}${arg ? ` ${arg}` : ""}?`, "error");
|
|
1794
1915
|
return info(`Unknown command /${command}. Type /help.`, "error");
|
|
1795
1916
|
}
|
|
1796
1917
|
}
|
|
@@ -1872,6 +1993,60 @@ ${snippet}
|
|
|
1872
1993
|
setQueued(queued.slice(take));
|
|
1873
1994
|
send(queued.slice(0, take).join("\n\n"));
|
|
1874
1995
|
}, [busy, approval, queued]);
|
|
1996
|
+
// When a reply points at places in the code (Nou's engine with show_places, the others with a ```nou-show
|
|
1997
|
+
// block, which is taken out of the reply), the first one opens highlighted and the rest are listed.
|
|
1998
|
+
useEffect(() => {
|
|
1999
|
+
if (busy || guideAsk.current)
|
|
2000
|
+
return;
|
|
2001
|
+
let stops = tourFound.current;
|
|
2002
|
+
tourFound.current = null;
|
|
2003
|
+
const reply = [...entries].reverse().find((e) => e.kind === "assistant");
|
|
2004
|
+
const found = reply && stopsFromReply(reply.text);
|
|
2005
|
+
if (found) {
|
|
2006
|
+
stops = stops?.length ? stops : found.stops;
|
|
2007
|
+
setEntries((prev) => prev.map((e) => (e === reply ? { ...e, text: found.rest || "Here's where." } : e)));
|
|
2008
|
+
}
|
|
2009
|
+
stops = (stops ?? []).filter((s) => existsSync(s.file));
|
|
2010
|
+
if (!stops.length)
|
|
2011
|
+
return;
|
|
2012
|
+
const list = stops.map((s, i) => `${i + 1}. ${s.file}:${s.line}${s.end > s.line ? `-${s.end}` : ""}${s.note ? ` · ${s.note}` : ""}`).join("\n");
|
|
2013
|
+
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" : ""}; /showme stop clears the highlights):\n${list}`);
|
|
2014
|
+
setTour({ stops, at: 0 });
|
|
2015
|
+
}, [busy]);
|
|
2016
|
+
// Each place: open it in the code view with its lines highlighted.
|
|
2017
|
+
useEffect(() => {
|
|
2018
|
+
const stop = tour?.stops[tour.at];
|
|
2019
|
+
if (!stop)
|
|
2020
|
+
return;
|
|
2021
|
+
openRef({ path: stop.file, line: stop.line, ...(stop.end > stop.line ? { to: stop.end } : {}) });
|
|
2022
|
+
}, [tour?.at, tour?.stops]);
|
|
2023
|
+
/** /showme <question>: explains it and opens the places in the code; /showme next|back|stop steps through them. */
|
|
2024
|
+
const showPlaces = (arg) => {
|
|
2025
|
+
const words = arg.trim();
|
|
2026
|
+
if (/^(stop|end|done|clear)$/.test(words)) {
|
|
2027
|
+
// Done looking: the highlighted lines go back to plain code.
|
|
2028
|
+
setTour(null);
|
|
2029
|
+
const highlighted = !!focusLine?.to;
|
|
2030
|
+
setFocusLine((f) => (f?.to !== undefined ? null : f));
|
|
2031
|
+
return info(tour || highlighted ? "Done showing: the highlights are cleared." : "Nothing is being shown.");
|
|
2032
|
+
}
|
|
2033
|
+
if (/^(next|back|prev|previous)$/.test(words)) {
|
|
2034
|
+
if (!tour)
|
|
2035
|
+
return info("Nothing is being shown. Ask with /showme <question>, e.g. /showme where do we check the login?");
|
|
2036
|
+
const at = tour.at + (words === "next" ? 1 : -1);
|
|
2037
|
+
if (at < 0 || at >= tour.stops.length)
|
|
2038
|
+
return info(at < 0 ? "That's the first place." : "That was the last place.");
|
|
2039
|
+
const stop = tour.stops[at];
|
|
2040
|
+
info(`📍 ${at + 1} of ${tour.stops.length}: ${stop.file}:${stop.line}${stop.end > stop.line ? `-${stop.end}` : ""}${stop.note ? ` · ${stop.note}` : ""}`);
|
|
2041
|
+
return setTour({ ...tour, at });
|
|
2042
|
+
}
|
|
2043
|
+
if (!words)
|
|
2044
|
+
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.)");
|
|
2045
|
+
if (busy)
|
|
2046
|
+
return info("Wait for the current reply to finish (or press Esc) first.", "error");
|
|
2047
|
+
setTour(null);
|
|
2048
|
+
send(explainPrompt(words, agentRef.current.id === "nou"));
|
|
2049
|
+
};
|
|
1875
2050
|
// When Nou has finished looking, the guide starts at its first step.
|
|
1876
2051
|
useEffect(() => {
|
|
1877
2052
|
if (busy || !guideAsk.current)
|
|
@@ -1897,8 +2072,8 @@ ${snippet}
|
|
|
1897
2072
|
info("It edited files anyway, so those changes were put back: you're the one typing them.");
|
|
1898
2073
|
}
|
|
1899
2074
|
if (!steps?.length)
|
|
1900
|
-
return info("Nou didn't come back with steps to show. Try asking more specifically, e.g. /
|
|
1901
|
-
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. /
|
|
2075
|
+
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");
|
|
2076
|
+
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.`);
|
|
1902
2077
|
setGuide({ steps, at: 0, task });
|
|
1903
2078
|
}, [busy]);
|
|
1904
2079
|
// Each step: open its file in the editor where the code goes, and say what to type and why.
|
|
@@ -1934,7 +2109,7 @@ ${snippet}
|
|
|
1934
2109
|
setGuide(null);
|
|
1935
2110
|
}
|
|
1936
2111
|
});
|
|
1937
|
-
/** /
|
|
2112
|
+
/** /teach <what you want>: Nou finds where the change goes and walks you through typing it. */
|
|
1938
2113
|
const showMe = (arg) => {
|
|
1939
2114
|
const words = arg.trim();
|
|
1940
2115
|
if (words === "stop" || words === "end") {
|
|
@@ -1952,8 +2127,8 @@ ${snippet}
|
|
|
1952
2127
|
}
|
|
1953
2128
|
if (!words) {
|
|
1954
2129
|
if (guide)
|
|
1955
|
-
return info(`On step ${guide.at + 1} of ${guide.steps.length}: ${guideStep.file}:${guideStep.line}. /
|
|
1956
|
-
return info("Tell Nou what you want to do and it shows you how, step by step, for you to type: /
|
|
2130
|
+
return info(`On step ${guide.at + 1} of ${guide.steps.length}: ${guideStep.file}:${guideStep.line}. /teach next skips it, /teach stop ends it.`);
|
|
2131
|
+
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");
|
|
1957
2132
|
}
|
|
1958
2133
|
if (busy)
|
|
1959
2134
|
return info("Wait for the current reply to finish (or press Esc) first.", "error");
|
|
@@ -1967,10 +2142,21 @@ ${snippet}
|
|
|
1967
2142
|
if (busy)
|
|
1968
2143
|
stopped.current = false;
|
|
1969
2144
|
}, [busy]);
|
|
1970
|
-
/**
|
|
1971
|
-
const
|
|
2145
|
+
/** Stops Nou and sends what's queued right away (a click on the queue, or /now). */
|
|
2146
|
+
const sendNow = () => {
|
|
2147
|
+
if (!queued.length)
|
|
2148
|
+
return;
|
|
2149
|
+
stop();
|
|
2150
|
+
stopped.current = false; // what's queued is meant to go
|
|
2151
|
+
};
|
|
2152
|
+
/**
|
|
2153
|
+
* From the chat: while Nou works, a message waits its turn, or (`now`: Alt+Enter) stops Nou and goes right
|
|
2154
|
+
* away. The "Send while Nou works" setting swaps the two, so Enter sends now and Alt+Enter queues.
|
|
2155
|
+
*/
|
|
2156
|
+
const submit = (text, alt = false) => {
|
|
1972
2157
|
if (!busy || (text.startsWith("/") && runsWhileBusy(text)))
|
|
1973
2158
|
return send(text);
|
|
2159
|
+
const now = settings.sendNowWhileBusy ? !alt : alt;
|
|
1974
2160
|
if (now) {
|
|
1975
2161
|
stop();
|
|
1976
2162
|
stopped.current = false; // this one is meant to go
|
|
@@ -2093,8 +2279,16 @@ ${snippet}
|
|
|
2093
2279
|
} }));
|
|
2094
2280
|
}
|
|
2095
2281
|
const height = Math.max(10, rows - 2);
|
|
2096
|
-
const
|
|
2097
|
-
const
|
|
2282
|
+
const baseTree = paneWidths ? Math.max(MIN_TREE, Math.round(columns * paneWidths.tree)) : Math.max(24, Math.floor(columns * 0.2));
|
|
2283
|
+
const baseChat = paneWidths ? Math.max(MIN_CHAT, Math.min(Math.round(columns * paneWidths.chat), columns - baseTree - MIN_CODE)) : Math.max(34, Math.floor(columns * 0.3));
|
|
2284
|
+
// Zooming the code panel: each step moves a few columns between it and the files and chat, as far as their
|
|
2285
|
+
// minimums (zooming in) or its own (zooming out) allow.
|
|
2286
|
+
const zoomCols = codeZoom * Math.max(4, Math.round(columns * 0.06));
|
|
2287
|
+
const spare = { tree: baseTree - MIN_TREE, chat: baseChat - MIN_CHAT };
|
|
2288
|
+
const taken = zoomCols > 0 ? Math.min(zoomCols, spare.tree + spare.chat) : -Math.min(-zoomCols, Math.max(0, columns - baseTree - baseChat - MIN_CODE));
|
|
2289
|
+
const fromTree = zoomCols > 0 ? Math.round(taken * (spare.tree / Math.max(1, spare.tree + spare.chat))) : Math.round(taken / 2);
|
|
2290
|
+
const treeWidth = baseTree - fromTree;
|
|
2291
|
+
const chatWidth = baseChat - (taken - fromTree);
|
|
2098
2292
|
const codeWidth = Math.max(MIN_CODE, columns - treeWidth - chatWidth);
|
|
2099
2293
|
const tabKey = tabs.length && openFile ? `${tabs.join("\0")}|${openFile}|${tabsWidth(codeWidth)}|${showEditor && !!editorState?.dirty}` : "";
|
|
2100
2294
|
if (tabStripCache.current?.key !== tabKey) {
|
|
@@ -2129,6 +2323,51 @@ ${snippet}
|
|
|
2129
2323
|
forgetScreen();
|
|
2130
2324
|
setRepaint((n) => n + 1);
|
|
2131
2325
|
}
|
|
2326
|
+
/** /workspace: list them, `new [folder]` opens one, a number switches. */
|
|
2327
|
+
async function workspaceCommand(arg) {
|
|
2328
|
+
const [sub, ...more] = arg.split(/\s+/);
|
|
2329
|
+
const tabs = workspaceTabs();
|
|
2330
|
+
if (!sub) {
|
|
2331
|
+
if (!isHosted())
|
|
2332
|
+
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).`);
|
|
2333
|
+
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.`);
|
|
2334
|
+
}
|
|
2335
|
+
if (/^\d+$/.test(sub)) {
|
|
2336
|
+
const n = Number(sub);
|
|
2337
|
+
if (!isHosted() || !tabs[n - 1])
|
|
2338
|
+
return info(`There's no workspace ${n}. /workspace lists them.`, "error");
|
|
2339
|
+
return void sendToHost({ type: "switch", index: n - 1 });
|
|
2340
|
+
}
|
|
2341
|
+
if (sub !== "new")
|
|
2342
|
+
return info("Try /workspace new <folder>, /workspace <n> or /workspace.", "error");
|
|
2343
|
+
const wanted = more.join(" ");
|
|
2344
|
+
if (isHosted())
|
|
2345
|
+
return void sendToHost({ type: "new", folder: wanted });
|
|
2346
|
+
const folder = workspaceFolder(wanted, process.cwd());
|
|
2347
|
+
if (!folder)
|
|
2348
|
+
return info(`No such folder: ${wanted}`, "error");
|
|
2349
|
+
if (busy)
|
|
2350
|
+
return info("Wait for the current reply to finish (or press Esc) first: Nou reopens as a workspaces window.", "error");
|
|
2351
|
+
// The pseudo-terminals workspaces run in come from a small native package, installed once like an engine.
|
|
2352
|
+
const pty = { id: "nou", label: "workspaces", description: "", approvals: "before", model: () => "", package: PTY_PACKAGE };
|
|
2353
|
+
if (!isInstalled(pty)) {
|
|
2354
|
+
info(`Workspaces need ${PTY_PACKAGE} (pseudo-terminals, prebuilt for macOS, Linux and Windows). Installing it once into ~/.nou/engines...`);
|
|
2355
|
+
setBusy("Installing workspaces support (npm)...");
|
|
2356
|
+
try {
|
|
2357
|
+
await installEngine(pty, (line) => setBusy(`Installing workspaces support (npm): ${line.slice(0, 70)}`));
|
|
2358
|
+
}
|
|
2359
|
+
catch (error) {
|
|
2360
|
+
return info(`Couldn't install ${PTY_PACKAGE}: ${error.message}`, "error");
|
|
2361
|
+
}
|
|
2362
|
+
finally {
|
|
2363
|
+
setBusy(null);
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
if (draft.current && settings.autoSave !== "off")
|
|
2367
|
+
saveFromEditor(draft.current.text, draft.current.path);
|
|
2368
|
+
reopenAsWorkspaces([process.cwd(), folder]);
|
|
2369
|
+
exit();
|
|
2370
|
+
}
|
|
2132
2371
|
/** Opens the terminal panel (starting its shell the first time) and gives it the keyboard, or closes it. */
|
|
2133
2372
|
function toggleTerminal(open = !termOpen) {
|
|
2134
2373
|
shellRef.current ??= new Shell(() => setTermVersion((v) => v + 1));
|
|
@@ -2185,7 +2424,7 @@ ${snippet}
|
|
|
2185
2424
|
// Everything Nou can do, for the command palette (? or Ctrl+P then >), with the key that does it.
|
|
2186
2425
|
const paletteActions = [
|
|
2187
2426
|
{ title: "Find a file", keys: "Ctrl+P", run: () => setQuickOpen("") },
|
|
2188
|
-
{ title: "Find in the open file", keys:
|
|
2427
|
+
{ title: "Find in the open file", keys: FIND_KEY, run: startFind },
|
|
2189
2428
|
{ title: "Open a file at a line", keys: "/open", run: () => typeCommand("/open ") },
|
|
2190
2429
|
{ title: "Go back to the previous file", keys: "b", run: goBack },
|
|
2191
2430
|
{ title: "Cancel your last message (edit it and resend)", keys: "/cancel", run: cancelLast },
|
|
@@ -2251,7 +2490,7 @@ ${snippet}
|
|
|
2251
2490
|
: showEditor && focus === "code"
|
|
2252
2491
|
? editorState?.selection
|
|
2253
2492
|
? [["Ctrl+C", "copy"], ["Ctrl+X", "cut"], ["Tab", "indent"], ["Ctrl+/", "comment"], ["Esc", "deselect"]]
|
|
2254
|
-
: [["Esc", "done"], ["Ctrl+S", settings.autoSave === "off" ? "save" : "save (auto-save on)"], ["Ctrl+Z", "undo"], ["Shift+→", "select"], [
|
|
2493
|
+
: [["Esc", "done"], ["Ctrl+S", settings.autoSave === "off" ? "save" : "save (auto-save on)"], ["Ctrl+Z", "undo"], ["Shift+→", "select"], [FIND_KEY, "find"], ["Ctrl+/", "comment"], ["/help", "all keys"]]
|
|
2255
2494
|
: focus === "chat"
|
|
2256
2495
|
? chatMenu ? [["↑↓", "choose"], ["Tab", "complete"], ["Enter", "run"], ["Esc", "close"]]
|
|
2257
2496
|
: busy
|
|
@@ -2261,9 +2500,9 @@ ${snippet}
|
|
|
2261
2500
|
? [["Enter", "open"], ["e", "edit"], ["/", "command"], ["c", "changes"], ["Ctrl+P", "find"], ["u", "undo"], ["p", "problems"], ["< >", "resize"], ["m", "select text"], ["s", "settings"]]
|
|
2262
2501
|
: diff
|
|
2263
2502
|
? [["Esc", "close diff"], ["n/N", "next/prev change"], ...(diffMode === "session" ? [["[ ]", "prev/next file"]] : []), ["v", "cycle diff"]]
|
|
2264
|
-
: [["double-click/e", "edit"], [
|
|
2503
|
+
: [["double-click/e", "edit"], [FIND_KEY, "find"], ["v", "diff"], ...(recent.length > 1 ? [["b", "back"]] : []), ["u", "undo"], ["/", "command"], ["p", "problems"], ["< >", "resize"], ["m", "select text"], ["s", "settings"]];
|
|
2265
2504
|
const followChip = follow ? ` ◉ follow${followLag ? ` ·${followLag}` : ""} ` : " ○ follow ";
|
|
2266
|
-
const statusUsed = mode.label.length + 2 + autoMode.length + 2 + followChip.length + `
|
|
2505
|
+
const statusUsed = mode.label.length + 2 + autoMode.length + 2 + followChip.length + ` ✗ ${totals.errors} ▲ ${totals.warnings} `.length + (checking.length ? 4 + checking.join(", ").length : 0) + 2;
|
|
2267
2506
|
const shownHints = [];
|
|
2268
2507
|
for (let used = statusUsed, i = 0; i < hints.length; i++) {
|
|
2269
2508
|
used += hints[i][0].length + hints[i][1].length + 4;
|
|
@@ -2276,7 +2515,7 @@ ${snippet}
|
|
|
2276
2515
|
y: () => answer(true), n: approval ? () => answer(false) : undefined, o: openPending,
|
|
2277
2516
|
"Shift+Tab": () => cycleAutoMode(), "/": () => typeCommand("/"), "Ctrl+P": () => setQuickOpen(""),
|
|
2278
2517
|
Tab: () => setFocus((f) => FOCUS_ORDER[(FOCUS_ORDER.indexOf(f) + 1) % FOCUS_ORDER.length]),
|
|
2279
|
-
e: startEditing, "double-click/e": startEditing, b: goBack,
|
|
2518
|
+
e: startEditing, "double-click/e": startEditing, b: goBack, [FIND_KEY]: showEditor ? undefined : startFind, v: cycleDiff, u: undoLast, p: toggleProblems, m: toggleMouse,
|
|
2280
2519
|
s: () => setView("settings"), "/help": () => void runCommand("/help"),
|
|
2281
2520
|
Esc: busy ? stop : diff && !showEditor ? () => setDiffMode("off") : undefined,
|
|
2282
2521
|
};
|
|
@@ -2290,7 +2529,7 @@ ${snippet}
|
|
|
2290
2529
|
};
|
|
2291
2530
|
add(` ${autoMode} `.length, () => cycleAutoMode());
|
|
2292
2531
|
add(followChip.length, () => setFollow((f) => !f));
|
|
2293
|
-
add(`
|
|
2532
|
+
add(` ✗ ${totals.errors} ▲ ${totals.warnings} `.length, toggleProblems);
|
|
2294
2533
|
if (checking.length)
|
|
2295
2534
|
add(3 + checking.join(", ").length);
|
|
2296
2535
|
if (!exitArmed) {
|
|
@@ -2301,10 +2540,10 @@ ${snippet}
|
|
|
2301
2540
|
}
|
|
2302
2541
|
}
|
|
2303
2542
|
const modelLabel = agentRef.current.id === "nou" ? describe(choice) : `${engineInfo(agentRef.current.id).label} · ${engineInfo(agentRef.current.id).model(choice)}`;
|
|
2304
|
-
clickable.current = { status: zones, modelFrom: columns - ("
|
|
2305
|
-
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, 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, 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:
|
|
2543
|
+
clickable.current = { status: zones, modelFrom: columns - (" LOCAL ".length + ` ${modelLabel} `.length + 1) };
|
|
2544
|
+
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, ...(tourFiles ? { showing: tourFiles } : {}), 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, { ...(tour && tour.stops[tour.at]?.file === openFile && !diff ? { showing: `${tour.at + 1}/${tour.stops.length}` } : {}), ...(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
|
|
2306
2545
|
? _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)` : ""] })
|
|
2307
|
-
: 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(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: paletteActions, initialQuery: quickOpen, onPick: (file) => {
|
|
2546
|
+
: 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) => {
|
|
2308
2547
|
setQuickOpen(null);
|
|
2309
2548
|
clearFollow();
|
|
2310
2549
|
open(file);
|
|
@@ -2312,6 +2551,10 @@ ${snippet}
|
|
|
2312
2551
|
setFocus("code");
|
|
2313
2552
|
}, onRun: (action) => {
|
|
2314
2553
|
setQuickOpen(null);
|
|
2554
|
+
setResumePicker(null);
|
|
2315
2555
|
action.run();
|
|
2316
|
-
}, onClose: () =>
|
|
2556
|
+
}, onClose: () => {
|
|
2557
|
+
setQuickOpen(null);
|
|
2558
|
+
setResumePicker(null);
|
|
2559
|
+
} }))] }));
|
|
2317
2560
|
}
|