nou-cli 0.5.11 → 0.5.13
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 +6 -6
- package/dist/src/cli.js +13 -3
- package/dist/src/games/index.js +6 -6
- package/dist/src/ui/App.js +104 -21
- package/dist/src/ui/Chat.js +35 -19
- package/dist/src/ui/ChatInput.js +28 -5
- package/dist/src/ui/CodeView.js +4 -3
- package/dist/src/ui/Editor.js +4 -3
- package/dist/src/ui/FileTree.js +3 -2
- package/dist/src/ui/Header.js +5 -2
- package/dist/src/ui/ProblemsView.js +4 -2
- package/dist/src/ui/QuickOpen.js +4 -2
- package/dist/src/ui/Settings.js +7 -7
- package/dist/src/ui/Terminal.js +3 -1
- package/dist/src/ui/background.js +27 -18
- package/dist/src/ui/commands.js +3 -1
- package/dist/src/ui/fast-width.js +5 -0
- package/dist/src/ui/keys.js +3 -4
- package/dist/src/ui/line-update-shim.js +55 -0
- package/dist/src/ui/scroll.js +27 -0
- package/dist/src/ui/text.js +1 -1
- package/dist/src/ui/theme.js +1 -1
- package/dist/src/workspaces/client.js +2 -0
- package/dist/src/workspaces/host.js +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,8 +50,8 @@ Needs **Node 22.12+** and an interactive terminal (Windows Terminal, PowerShell,
|
|
|
50
50
|
| 🔀 **Live diffs** | Every edit Nou proposes shows up side by side, and the files it is editing shimmer in the file tree while it works. <kbd>y</kbd> accepts, <kbd>n</kbd> rejects. Nothing touches disk without you. |
|
|
51
51
|
| ✍️ **Built-in editor** | Press <kbd>e</kbd> to edit the open file yourself, with undo/redo and **auto-save**. |
|
|
52
52
|
| 📍 **Shows you where things are** | Ask "where is the LiveKit integration?" and Nou finds it and opens it in the middle panel, jumped to the exact lines and highlighted. Any file a reply mentions (`src/a.ts:12`) is a link: click it. `/open name[:line]` opens a file yourself. |
|
|
53
|
-
| 🪟 **Workspaces** | Several projects in one window, a tab each in the header: click **+** (or `/workspace new ../api`)
|
|
54
|
-
| 🗂️ **Several agents at once** | `/new` (or **+** in the chat's title) opens another conversation tab while the first keeps working. Each tab has its own agent and history; a working tab shows ●, one waiting for your OK shows !, and a click (or `/tab 2`) switches. `/close` closes one; it stays in `/resume`. |
|
|
53
|
+
| 🪟 **Workspaces** | Several projects in one window, a tab each in the header: click **+** and pick a folder (or `/workspace new ../api`): it opens beside this one with its own files, chats and agents. Click a tab (or `/workspace 2`) to switch; the ones in the background keep working, and a tab shows ● while its agent is busy. A tab's **×** closes it (so does quitting it, Ctrl+C twice). The first time, Nou installs a small pseudo-terminal package ([@lydell/node-pty](https://www.npmjs.com/package/@lydell/node-pty), prebuilt for macOS, Linux and Windows) and reopens as a workspaces window, with your conversation in the first tab. Or start that way: `nou --workspaces . ../api`. |
|
|
54
|
+
| 🗂️ **Several agents at once** | `/new` (or **+** in the chat's title) opens another conversation tab while the first keeps working. Each tab has its own agent and history; a working tab shows ●, one waiting for your OK shows !, and a click (or `/tab 2`) switches. A tab's **×** (or `/close`) closes it, even one in the background; it stays in `/resume`. |
|
|
55
55
|
| 👀 **Show me** | Ask how something works or where it happens (`/showme where do we check the login?`, or just ask) and Nou explains it with the code open: each place it talks about opens in the code view, highlighted, and they're listed in the chat to click. `/showme next` and `/showme back` step through them, and `/showme stop` clears the highlights. While it shows, the code view and the file tree take its purple (◉ SHOWING 2/6, and each file's places), as they take green while you edit. Works with every engine: Nou's points at places with a tool, and Claude Code, Codex and pi are asked to list them at the end of their reply, which Nou reads and hides. |
|
|
56
56
|
| 🧑🏫 **Teach me** | `/teach add a --verbose flag` and Nou finds where the change goes, then walks you through typing it yourself: each step opens the file at the right line, says why, and shows the code to type dim ahead of your cursor, like autocomplete (<kbd>Tab</kbd> fills in a line). It moves on when what you typed matches, and saves as it goes. `/teach next` skips a step, `/teach stop` ends it. Works with every engine (Nou's, Claude Code, Codex and pi): any edit one makes anyway is put back. |
|
|
57
57
|
| 🎮 **Games for a break** | Add Pong, Snake, Breakout (it gets faster each wall you clear), Dodge, Hop and Pairs in settings → Games, then `/games` opens a panel docked under the chat: it splits that column instead of covering anything. Drag its title onto the files, code or chat to dock it there, and × at its top right closes it. Drag its top edge to make it taller or shorter. A game runs only while the panel has the keyboard: Esc goes back to your code and it waits. Your best score in each is kept, and a panel too small for a game says how much room it needs. |
|
|
@@ -69,7 +69,7 @@ Needs **Node 22.12+** and an interactive terminal (Windows Terminal, PowerShell,
|
|
|
69
69
|
| 🎚️ **Speed vs smarts** | Pick how hard the model thinks in plain words: **Fastest** and **Fast** (cheaper + faster), **Balanced** (the default), **Smarter** and **Smartest** (thinks longer, costs more). In <kbd>s</kbd> settings, or `/effort smarter`. |
|
|
70
70
|
| 🛞 **Pick your agent** | Nou's own, or Claude Code, Codex or pi, switchable like models, with the same diffs, approvals and undo. |
|
|
71
71
|
| 🧩 **Plugins** | Add your own tools and slash commands with a single file. |
|
|
72
|
-
| 🔎 **Quick open** | <kbd>Ctrl</kbd>+<kbd>P</kbd> fuzzy-finds any file. |
|
|
72
|
+
| 🔎 **Quick open** | <kbd>Ctrl</kbd>+<kbd>P</kbd> (or `/files`) fuzzy-finds any file; <kbd>?</kbd> (or `/actions`) lists every action. |
|
|
73
73
|
| 🔋 **Easy on your battery** | Idle, Nou uses next to no CPU. When its window is in the background (in terminals that report focus: iTerm2, Terminal.app, kitty, Ghostty, WezTerm, Windows Terminal, VS Code), spinners and git polling pause until you come back, and TypeScript checks are incremental. |
|
|
74
74
|
| 👓 **Easy to read** | Hints and secondary text keep at least 4.5:1 contrast. Nou asks your terminal whether its background is light or dark and picks matching colors (code switches to GitHub Light on a light background); set it yourself in <kbd>s</kbd> settings → **Nou's colors**. |
|
|
75
75
|
|
|
@@ -94,7 +94,7 @@ Every change is reviewed like a code review:
|
|
|
94
94
|
| <kbd>Tab</kbd> | switch between files, code and chat |
|
|
95
95
|
| <kbd>Shift</kbd>+<kbd>Tab</kbd> | ask → auto-edit (edits apply without asking) → auto (commands too); everything stays undoable |
|
|
96
96
|
| <kbd>Ctrl</kbd>+<kbd>P</kbd> | fuzzy-find a file; the files you've opened are listed first |
|
|
97
|
-
| <kbd>+</kbd> / <kbd>-</kbd>
|
|
97
|
+
| <kbd>Ctrl</kbd>+<kbd>=</kbd> / <kbd>Ctrl</kbd>+<kbd>-</kbd> from anywhere (or <kbd>+</kbd> / <kbd>-</kbd> in the code or files) | widen or narrow the code panel, a step at a time: the files and chat give or take the room (a terminal can't change one pane's text size); <kbd>Ctrl</kbd>+<kbd>0</kbd> or <kbd>=</kbd> puts it back. Ctrl+= and Ctrl+- need a terminal with the kitty keyboard protocol (Ghostty, kitty, WezTerm, iTerm2). For bigger text, use your terminal's own zoom |
|
|
98
98
|
| <kbd>b</kbd> | back to the previous file (press again to flip back) |
|
|
99
99
|
| <kbd>Ctrl</kbd>+<kbd>F</kbd> (<kbd>Cmd</kbd>+<kbd>F</kbd> on a Mac, in terminals that report Cmd: Ghostty, kitty, WezTerm, iTerm2) | find in the open file, from any pane: matches are highlighted and counted; <kbd>Enter</kbd>/<kbd>↓</kbd> next, <kbd>↑</kbd> previous, <kbd>Esc</kbd> close |
|
|
100
100
|
| 🗂️ Tabs | every file you open gets a tab over the code: click one to switch, any tab's × to close it, ‹ › for ones that don't fit · <kbd>[</kbd>/<kbd>]</kbd> previous/next · <kbd>x</kbd> or <kbd>Ctrl</kbd>+<kbd>W</kbd> close |
|
|
@@ -131,11 +131,11 @@ In the editor:
|
|
|
131
131
|
| <kbd>Ctrl</kbd>+<kbd>K</kbd> · <kbd>Ctrl</kbd>+<kbd>G</kbd> | hover info · go to definition (with a language server) |
|
|
132
132
|
| <kbd>Esc</kbd> | clear the selection, or leave the editor |
|
|
133
133
|
|
|
134
|
-
Chat commands: `/settings` · `/model [provider:model]` · `/models [provider]` · `/pull <model>` · `/engine [name]` · `/effort [fastest|fast|balanced|smarter|smartest]` · `/auto [ask|auto-edit|auto]` · `/copy [code|file]` · `/undo` · `/rewind` · `/history` · `/diff [file or n]` · `/sessions` · `/problems` · `/compact` · `/clear` · `/resume [n|last]` · `/terminal` · `/zoom` · `/workspace [new <folder> | n]` · `/now` · `/showme <question>` · `/teach <change>` · `/help`
|
|
134
|
+
Chat commands: `/settings` · `/model [provider:model]` · `/models [provider]` · `/pull <model>` · `/engine [name]` · `/effort [fastest|fast|balanced|smarter|smartest]` · `/auto [ask|auto-edit|auto]` · `/copy [code|file]` · `/undo` · `/rewind` · `/history` · `/diff [file or n]` · `/sessions` · `/problems` · `/compact` · `/clear` · `/resume [n|last]` · `/terminal` · `/zoom` · `/workspace [new <folder> | n]` · `/files` · `/actions` · `/now` · `/showme <question>` · `/teach <change>` · `/help`
|
|
135
135
|
|
|
136
136
|
A small typo in a command still works when it only changes the screen (`/termnial` opens the terminal); for others Nou asks "Did you mean...?" instead of guessing.
|
|
137
137
|
|
|
138
|
-
New lines in a message: <kbd>Shift</kbd>/<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Enter</kbd> in terminals that report them (kitty, Ghostty, WezTerm, iTerm2 and others with the kitty keyboard protocol), or type `\` and press <kbd>Enter</kbd> in any terminal.
|
|
138
|
+
New lines in a message: <kbd>Shift</kbd>/<kbd>Ctrl</kbd>/<kbd>Cmd</kbd>+<kbd>Enter</kbd> in terminals that report them (kitty, Ghostty, WezTerm, iTerm2 and others with the kitty keyboard protocol, which Nou checks for before it starts; also xterm's modifyOtherKeys form), or type `\` and press <kbd>Enter</kbd> in any terminal.
|
|
139
139
|
|
|
140
140
|
Conversations are saved per folder after each reply. `/resume` opens a picker (↑↓ and Enter, type to filter, or type its number) and `/resume 2` (or `nou --continue` for the last one, `nou --resume` for the list) picks one back up with the engine it was on: Nou's own engine restores its messages, Claude Code its session and Codex its thread; pi is handed the conversation so far. Sessions you started in Claude Code's or Codex's own CLI in this folder are listed too (marked "CLI"), and resuming one carries it on in that engine inside Nou.
|
|
141
141
|
|
package/dist/src/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import { App } from "./ui/App.js";
|
|
|
15
15
|
import { installFastOutput } from "./ui/fast-output.js";
|
|
16
16
|
import { RENDER_OPTIONS } from "./ui/render-options.js";
|
|
17
17
|
import { setScreenClear } from "./ui/redraw.js";
|
|
18
|
-
import {
|
|
18
|
+
import { detectTerminal } from "./ui/background.js";
|
|
19
19
|
import { setPalette } from "./ui/theme.js";
|
|
20
20
|
import { runHost, workspaceFolder } from "./workspaces/host.js";
|
|
21
21
|
import { pendingWorkspaces } from "./workspaces/client.js";
|
|
@@ -96,7 +96,11 @@ async function main() {
|
|
|
96
96
|
console.error(`Not a folder, so not opened: ${missing.join(", ")}`);
|
|
97
97
|
const uiTheme = loadSettings().uiTheme;
|
|
98
98
|
// Every workspace uses the colors found for this terminal (they can't ask it themselves from the background).
|
|
99
|
-
|
|
99
|
+
// The host asks the terminal once and tells every workspace (in the background they can't ask it).
|
|
100
|
+
const term = await detectTerminal();
|
|
101
|
+
process.env.NOU_PALETTE = uiTheme === "dark" || uiTheme === "light" ? uiTheme : term.palette ?? "dark";
|
|
102
|
+
if (term.kitty !== null)
|
|
103
|
+
process.env.NOU_KITTY = term.kitty ? "1" : "0";
|
|
100
104
|
const firstArgs = args.includes("--continue") || args.includes("-c") ? ["--continue"] : [];
|
|
101
105
|
process.exit(await runHost(folders.length ? folders : [process.cwd()], firstArgs));
|
|
102
106
|
}
|
|
@@ -163,7 +167,13 @@ async function main() {
|
|
|
163
167
|
// Colors that are readable on this terminal: a dark or a light set, from the setting or the background.
|
|
164
168
|
const uiTheme = loadSettings().uiTheme;
|
|
165
169
|
const fromHost = process.env.NOU_PALETTE;
|
|
166
|
-
|
|
170
|
+
const hostKnows = fromHost === "light" || fromHost === "dark";
|
|
171
|
+
const term = hostKnows ? { palette: fromHost, kitty: process.env.NOU_KITTY ? process.env.NOU_KITTY === "1" : null } : await detectTerminal();
|
|
172
|
+
setPalette(uiTheme === "dark" || uiTheme === "light" ? uiTheme : term.palette ?? "dark");
|
|
173
|
+
// Turn the kitty keyboard protocol on when the terminal said it speaks it (Shift/Ctrl/Cmd+Enter, Cmd+F and
|
|
174
|
+
// Ctrl+= need it), off when it answered without it; ask the way Ink does only if it answered nothing.
|
|
175
|
+
if (term.kitty !== null && RENDER_OPTIONS.kittyKeyboard)
|
|
176
|
+
RENDER_OPTIONS.kittyKeyboard = { ...RENDER_OPTIONS.kittyKeyboard, mode: term.kitty ? "enabled" : "disabled" };
|
|
167
177
|
await installFastOutput();
|
|
168
178
|
// While Nou is on screen, anything a library prints (console.log, warnings, stderr) goes to a log file:
|
|
169
179
|
// written into the terminal, it would scroll the screen out from under the mouse.
|
package/dist/src/games/index.js
CHANGED
|
@@ -221,7 +221,7 @@ export const breakout = {
|
|
|
221
221
|
return {
|
|
222
222
|
lines: grid.map((r) => r.join("")),
|
|
223
223
|
score: s.score,
|
|
224
|
-
status: `level ${s.level} · score ${s.score} · lives ${"
|
|
224
|
+
status: `level ${s.level} · score ${s.score} · lives ${"◆".repeat(Math.max(0, s.lives))}`,
|
|
225
225
|
...(s.lives <= 0 ? { over: `Game over: ${s.score} points, level ${s.level}.` } : {}),
|
|
226
226
|
};
|
|
227
227
|
},
|
|
@@ -230,13 +230,13 @@ export const breakout = {
|
|
|
230
230
|
export const dodge = {
|
|
231
231
|
id: "dodge",
|
|
232
232
|
title: "Dodge",
|
|
233
|
-
blurb: "Rocks fall faster and thicker the longer you last. Catch
|
|
233
|
+
blurb: "Rocks fall faster and thicker the longer you last. Catch ✦ for points.",
|
|
234
234
|
controls: "←→ or a/d move",
|
|
235
235
|
tickMs: 90,
|
|
236
236
|
minWidth: 12,
|
|
237
237
|
minHeight: 6,
|
|
238
238
|
speed: (s) => Math.max(45, 90 - Math.floor(s.ticks / 60) * 5),
|
|
239
|
-
colors: { "▲": "#61afef", "●": "#d19a66", "
|
|
239
|
+
colors: { "▲": "#61afef", "●": "#d19a66", "✦": "#e5c07b" },
|
|
240
240
|
create(w, h, seed = Date.now()) {
|
|
241
241
|
[w, h] = [Math.max(w, this.minWidth), Math.max(h, this.minHeight)];
|
|
242
242
|
return { w, h, x: Math.floor(w / 2), rocks: [], stars: [], score: 0, ticks: 0, dead: false, random: rng(seed) };
|
|
@@ -271,7 +271,7 @@ export const dodge = {
|
|
|
271
271
|
for (const r of s.rocks)
|
|
272
272
|
put(grid, r.x, r.y, "●");
|
|
273
273
|
for (const r of s.stars)
|
|
274
|
-
put(grid, r.x, r.y, "
|
|
274
|
+
put(grid, r.x, r.y, "✦");
|
|
275
275
|
put(grid, s.x, s.h - 1, "▲");
|
|
276
276
|
return { lines: grid.map((r) => r.join("")), score: s.score, status: `score ${s.score}`, ...(s.dead ? { over: `Hit! ${s.score} points.` } : {}) };
|
|
277
277
|
},
|
|
@@ -322,7 +322,7 @@ export const hop = {
|
|
|
322
322
|
return { lines: grid.map((r) => r.join("")), score: s.score, status: `walls hopped ${s.score}`, ...(s.dead ? { over: `Bonk! ${s.score} hopped.` } : {}) };
|
|
323
323
|
},
|
|
324
324
|
};
|
|
325
|
-
const SYMBOLS = ["◆", "●", "▲", "■", "
|
|
325
|
+
const SYMBOLS = ["◆", "●", "▲", "■", "✦", "◈", "⊕", "⊗", "◎", "△", "○", "✧"];
|
|
326
326
|
/** Cards face down: flip two at a time and find every matching pair in as few moves as you can. */
|
|
327
327
|
export const pairs = {
|
|
328
328
|
id: "pairs",
|
|
@@ -332,7 +332,7 @@ export const pairs = {
|
|
|
332
332
|
tickMs: 100,
|
|
333
333
|
minWidth: 16,
|
|
334
334
|
minHeight: 5,
|
|
335
|
-
colors: { "◆": "#e06c75", "●": "#e5c07b", "▲": "#61afef", "■": "#98c379", "
|
|
335
|
+
colors: { "◆": "#e06c75", "●": "#e5c07b", "▲": "#61afef", "■": "#98c379", "✦": "#e5c07b", "◈": "#e06c75", "⊕": "#98c379", "⊗": "#c678dd", "◎": "#d19a66", "△": "#56b6c2", "○": "#c678dd", "✧": "#56b6c2", "▒": "#5c6370", "▸": "#e5c07b", "◂": "#e5c07b" },
|
|
336
336
|
create(w, h, seed = Date.now()) {
|
|
337
337
|
[w, h] = [Math.max(w, this.minWidth), Math.max(h, this.minHeight)];
|
|
338
338
|
// As many cards as fit (each 4 wide, rows 2 apart), an even number, up to every symbol twice.
|
package/dist/src/ui/App.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
4
|
+
import os from "os";
|
|
4
5
|
import path from "path";
|
|
5
6
|
import { Box, Text, useApp, useInput, useWindowSize } from "ink";
|
|
6
7
|
import { nouRulesOnly } from "../context/index.js";
|
|
@@ -35,7 +36,7 @@ import { FileTree } from "./FileTree.js";
|
|
|
35
36
|
import { CodeView } from "./CodeView.js";
|
|
36
37
|
import { Editor } from "./Editor.js";
|
|
37
38
|
import { SLASH_COMMANDS, closestCommand, onlyChangesScreen, runsWhileBusy } from "./commands.js";
|
|
38
|
-
import { WHEEL_STEP } from "./scroll.js";
|
|
39
|
+
import { WHEEL_STEP, sendWheel } from "./scroll.js";
|
|
39
40
|
import { Chat, chatTabZones } from "./Chat.js";
|
|
40
41
|
import { copyToClipboard, openInBrowser } from "./clipboard.js";
|
|
41
42
|
import { Header, headerZones } from "./Header.js";
|
|
@@ -124,6 +125,8 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
124
125
|
const [quickOpen, setQuickOpen] = useState(null);
|
|
125
126
|
/** /resume's picker: the conversations, in the palette's place while it's open. */
|
|
126
127
|
const [resumePicker, setResumePicker] = useState(null);
|
|
128
|
+
/** What the open picker is for, said in its empty search box. */
|
|
129
|
+
const [pickerTitle, setPickerTitle] = useState("Resume a conversation");
|
|
127
130
|
const [focus, setFocus] = useState("chat");
|
|
128
131
|
/** The Games panel: whether it's open, the pane it's being dragged over, and the pane to go back to. */
|
|
129
132
|
const [gamesOpen, setGamesOpen] = useState(false);
|
|
@@ -245,7 +248,6 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
245
248
|
const [pendingNote, setPendingNote] = useState(null);
|
|
246
249
|
const [exitArmed, setExitArmed] = useState(false);
|
|
247
250
|
const [update, setUpdate] = useState(null);
|
|
248
|
-
const [wheel, setWheel] = useState(null);
|
|
249
251
|
const [scrollTo, setScrollTo] = useState(null);
|
|
250
252
|
const [editorMouse, setEditorMouse] = useState([]);
|
|
251
253
|
/** Wheel events waiting for the next frame, added up per pane. */
|
|
@@ -284,6 +286,16 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
284
286
|
* wider (taken from the files and chat) or narrower (given to them). 0 is the normal layout.
|
|
285
287
|
*/
|
|
286
288
|
const [codeZoom, setCodeZoom] = useState(0);
|
|
289
|
+
// A resized terminal is drawn again in full: drawing only the lines that changed, over a screen the
|
|
290
|
+
// terminal has rewrapped, can leave pieces of the old layout behind.
|
|
291
|
+
const firstSize = useRef(true);
|
|
292
|
+
useEffect(() => {
|
|
293
|
+
if (firstSize.current) {
|
|
294
|
+
firstSize.current = false;
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
redraw();
|
|
298
|
+
}, [columns, rows]);
|
|
287
299
|
const zoomCode = (by) => setCodeZoom((z) => (by === 0 ? 0 : Math.max(-6, Math.min(8, z + by))));
|
|
288
300
|
const paneWidthsRef = useRef(paneWidths);
|
|
289
301
|
paneWidthsRef.current = paneWidths;
|
|
@@ -462,10 +474,20 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
462
474
|
switchChatTab(id);
|
|
463
475
|
}
|
|
464
476
|
/** Closes the conversation tab on screen (stopping its engine); the conversation stays saved for /resume. */
|
|
465
|
-
function closeChatTab() {
|
|
477
|
+
function closeChatTab(id = activeTab.current) {
|
|
466
478
|
if (chatTabs.length < 2)
|
|
467
479
|
return info("That's the only conversation tab. /clear starts a fresh conversation in it.");
|
|
468
|
-
|
|
480
|
+
if (id !== activeTab.current) {
|
|
481
|
+
// A tab in the background (its ×): stop its agent and drop it; the one on screen stays.
|
|
482
|
+
const t = tabStore.current.get(id);
|
|
483
|
+
t?.approval?.resolve(false);
|
|
484
|
+
t?.agent.cancel();
|
|
485
|
+
t?.agent.dispose?.();
|
|
486
|
+
tabStore.current.delete(id);
|
|
487
|
+
tabHooks.current.delete(id);
|
|
488
|
+
setChatTabs((tabs) => tabs.filter((x) => x !== id));
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
469
491
|
const at = chatTabs.indexOf(id);
|
|
470
492
|
const next = chatTabs[at + 1] ?? chatTabs[at - 1];
|
|
471
493
|
const agent = agentRef.current;
|
|
@@ -621,6 +643,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
621
643
|
redraw();
|
|
622
644
|
},
|
|
623
645
|
error: (text) => info(text, "error"),
|
|
646
|
+
quit: () => quit(),
|
|
624
647
|
});
|
|
625
648
|
}, []);
|
|
626
649
|
useEffect(() => {
|
|
@@ -694,6 +717,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
694
717
|
info(`Earlier conversations here (also in the picker: ↑↓ and Enter, or type a number here):\n${rows.join("\n")}`);
|
|
695
718
|
// A picker over the screen: ↑↓ and Enter, or type to filter by title.
|
|
696
719
|
setResumePicker(list.map((c, i) => ({ title: `${i + 1}. ${c.title}`, keys: `${where(c)} · ${ago(c.updated)}`, run: () => void resumePicked(c) })));
|
|
720
|
+
setPickerTitle("Resume a conversation · ↑↓ and Enter, or type its number");
|
|
697
721
|
setQuickOpen(">");
|
|
698
722
|
return;
|
|
699
723
|
}
|
|
@@ -880,7 +904,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
880
904
|
return;
|
|
881
905
|
const added = newProblems(before, after);
|
|
882
906
|
setPendingNote(added.length
|
|
883
|
-
? { short:
|
|
907
|
+
? { short: `▲ +${added.length} problem${added.length === 1 ? "" : "s"}`, detail: added.slice(0, 3).map((d) => formatDiagnostic(d)).join("\n") }
|
|
884
908
|
: { short: "✓ no new problems", detail: null });
|
|
885
909
|
})();
|
|
886
910
|
return () => {
|
|
@@ -907,6 +931,9 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
907
931
|
const tree = border === "tree" ? Math.min(Math.max(x + 1, MIN_TREE), columns - chatWidth - MIN_CODE) : treeWidth;
|
|
908
932
|
const chat = border === "chat" ? Math.min(Math.max(columns - x, MIN_CHAT), columns - treeWidth - MIN_CODE) : chatWidth;
|
|
909
933
|
const next = { tree: tree / columns, chat: chat / columns };
|
|
934
|
+
// Widths come from the panes as they are on screen (zoom included), so the zoom folds into them: the
|
|
935
|
+
// border lands where it's dropped instead of a zoom step away.
|
|
936
|
+
setCodeZoom(0);
|
|
910
937
|
setPaneWidths(next);
|
|
911
938
|
return next;
|
|
912
939
|
};
|
|
@@ -946,7 +973,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
946
973
|
const { pane } = q;
|
|
947
974
|
q.lines -= lines;
|
|
948
975
|
q.columns -= columns;
|
|
949
|
-
|
|
976
|
+
sendWheel(pane, lines, columns);
|
|
950
977
|
if (q.lines || q.columns)
|
|
951
978
|
q.timer = setTimeout(flushWheel, 16);
|
|
952
979
|
};
|
|
@@ -1081,9 +1108,9 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1081
1108
|
if (event.kind === "click" && event.y === 0) {
|
|
1082
1109
|
const zone = headerZones(workspaceTabs()).find((z) => event.x >= z.from && event.x < z.to);
|
|
1083
1110
|
if (zone?.tab === "new")
|
|
1084
|
-
return
|
|
1111
|
+
return pickWorkspaceFolder();
|
|
1085
1112
|
if (typeof zone?.tab === "number")
|
|
1086
|
-
return void sendToHost({ type: "switch", index: zone.tab });
|
|
1113
|
+
return void sendToHost({ type: zone.close ? "close" : "switch", index: zone.tab });
|
|
1087
1114
|
}
|
|
1088
1115
|
if (event.kind === "click" && event.y === paneHeight + 1)
|
|
1089
1116
|
return clickable.current.status.find((z) => event.x >= z.from && event.x < z.to)?.run();
|
|
@@ -1117,6 +1144,8 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1117
1144
|
if (Math.abs(at - plus) <= 1)
|
|
1118
1145
|
return newChatTab();
|
|
1119
1146
|
const zone = zones.find((z) => at >= z.from && at < z.to);
|
|
1147
|
+
if (zone?.close)
|
|
1148
|
+
return closeChatTab(zone.id);
|
|
1120
1149
|
if (zone)
|
|
1121
1150
|
return switchChatTab(zone.id);
|
|
1122
1151
|
}
|
|
@@ -1484,6 +1513,12 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1484
1513
|
if (focus !== "chat" && input === "o" && pendingChange)
|
|
1485
1514
|
return openPending();
|
|
1486
1515
|
}
|
|
1516
|
+
// Zoom the code panel from anywhere: Ctrl+= / Ctrl+- / Ctrl+0, in terminals that report Ctrl with those
|
|
1517
|
+
// keys (the kitty keyboard protocol Nou asks for). Elsewhere Ctrl+- is the same key as Ctrl+/, so it's
|
|
1518
|
+
// left alone; + and - in the code or the files work in every terminal.
|
|
1519
|
+
if (key.ctrl && (input === "=" || input === "+" || input === "-" || input === "0") && !key.meta) {
|
|
1520
|
+
return zoomCode(input === "0" ? 0 : input === "-" ? -1 : 1);
|
|
1521
|
+
}
|
|
1487
1522
|
if (key.ctrl && input === "p") {
|
|
1488
1523
|
setQuickOpen("");
|
|
1489
1524
|
return;
|
|
@@ -1555,7 +1590,7 @@ export function App({ initialModel, permContext, resume }) {
|
|
|
1555
1590
|
setPaneWidths(null);
|
|
1556
1591
|
updateSettings({ paneWidths: null });
|
|
1557
1592
|
}
|
|
1558
|
-
else if ((input === "+" || input === "-") && focus === "code") {
|
|
1593
|
+
else if ((input === "+" || input === "-") && (focus === "code" || focus === "tree")) {
|
|
1559
1594
|
zoomCode(input === "+" ? 1 : -1);
|
|
1560
1595
|
}
|
|
1561
1596
|
else if (input === "v")
|
|
@@ -1606,7 +1641,7 @@ ${snippet}
|
|
|
1606
1641
|
</editor_context>`;
|
|
1607
1642
|
};
|
|
1608
1643
|
const contextChip = openFile
|
|
1609
|
-
? `${openFile}${showEditor && editorState ? `:${editorState.line}${editorState.dirty ? "
|
|
1644
|
+
? `${openFile}${showEditor && editorState ? `:${editorState.line}${editorState.dirty ? " ± unsaved" : ""}` : viewport ? `:${viewport.top}-${viewport.bottom}` : ""}`
|
|
1610
1645
|
: null;
|
|
1611
1646
|
/** /mcp: list servers; add (a preset, a command, or a URL), remove, or import the ones from other tools. */
|
|
1612
1647
|
const mcpCommand = async ([action, name, ...more]) => {
|
|
@@ -1682,7 +1717,7 @@ ${snippet}
|
|
|
1682
1717
|
}
|
|
1683
1718
|
if (action)
|
|
1684
1719
|
return info("Usage: /plugins, /plugins add <name>, /plugins new <name>", "error");
|
|
1685
|
-
const mark = { on: "●", off: "○", changed: "
|
|
1720
|
+
const mark = { on: "●", off: "○", changed: "▲" };
|
|
1686
1721
|
const lines = allPlugins.map((p) => p.scope === "starter"
|
|
1687
1722
|
? ` + ${p.id.padEnd(16)} ${p.plugin?.description ?? ""}`
|
|
1688
1723
|
: ` ${mark[trustState(p, settings)]} ${p.id.padEnd(16)} ${p.scope}${p.error ? ` · ✗ ${p.error}` : ""}`);
|
|
@@ -1856,6 +1891,11 @@ ${snippet}
|
|
|
1856
1891
|
if (/^(reset|0|off)$/.test(arg))
|
|
1857
1892
|
return zoomCode(0);
|
|
1858
1893
|
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 +).");
|
|
1894
|
+
case "files":
|
|
1895
|
+
return setQuickOpen("");
|
|
1896
|
+
case "actions":
|
|
1897
|
+
case "palette":
|
|
1898
|
+
return setQuickOpen(">");
|
|
1859
1899
|
case "workspace":
|
|
1860
1900
|
case "workspaces":
|
|
1861
1901
|
case "ws":
|
|
@@ -2010,7 +2050,7 @@ ${snippet}
|
|
|
2010
2050
|
if (!stops.length)
|
|
2011
2051
|
return;
|
|
2012
2052
|
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(
|
|
2053
|
+
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
2054
|
setTour({ stops, at: 0 });
|
|
2015
2055
|
}, [busy]);
|
|
2016
2056
|
// Each place: open it in the code view with its lines highlighted.
|
|
@@ -2037,7 +2077,7 @@ ${snippet}
|
|
|
2037
2077
|
if (at < 0 || at >= tour.stops.length)
|
|
2038
2078
|
return info(at < 0 ? "That's the first place." : "That was the last place.");
|
|
2039
2079
|
const stop = tour.stops[at];
|
|
2040
|
-
info(
|
|
2080
|
+
info(`◉ ${at + 1} of ${tour.stops.length}: ${stop.file}:${stop.line}${stop.end > stop.line ? `-${stop.end}` : ""}${stop.note ? ` · ${stop.note}` : ""}`);
|
|
2041
2081
|
return setTour({ ...tour, at });
|
|
2042
2082
|
}
|
|
2043
2083
|
if (!words)
|
|
@@ -2087,7 +2127,7 @@ ${snippet}
|
|
|
2087
2127
|
writeFileSync(step.file, "");
|
|
2088
2128
|
setTreeVersion((v) => v + 1);
|
|
2089
2129
|
}
|
|
2090
|
-
info(
|
|
2130
|
+
info(`▸ Step ${guide.at + 1} of ${guide.steps.length}: ${step.file}:${step.line}\n${step.why}\n\n\`\`\`\n${step.code}\n\`\`\``);
|
|
2091
2131
|
openAt(step.file, step.line, true);
|
|
2092
2132
|
}, [guide?.at, guide?.steps]);
|
|
2093
2133
|
const guideStep = guide?.steps[guide.at] ?? null;
|
|
@@ -2105,7 +2145,7 @@ ${snippet}
|
|
|
2105
2145
|
setGuide({ ...guide, steps, at: guide.at + 1 });
|
|
2106
2146
|
}
|
|
2107
2147
|
else {
|
|
2108
|
-
info(
|
|
2148
|
+
info(`✓ That's it: all ${steps.length} step${steps.length > 1 ? "s" : ""} done and saved. Ask Nou to check it over if you like.`);
|
|
2109
2149
|
setGuide(null);
|
|
2110
2150
|
}
|
|
2111
2151
|
});
|
|
@@ -2305,7 +2345,6 @@ ${snippet}
|
|
|
2305
2345
|
const dockLeft = gameDock === "tree" ? 0 : gameDock === "code" ? treeWidth : treeWidth + codeWidth;
|
|
2306
2346
|
const dockWidth = gameDock === "tree" ? treeWidth : gameDock === "code" ? codeWidth : chatWidth;
|
|
2307
2347
|
layout.current = { treeWidth, codeWidth, paneHeight: height, heights, game: gamesOpen ? { x: dockLeft, y: 1 + heights[gameDock] + (gameDock === "code" ? termHeight : 0), width: dockWidth, height: gameHeight } : null, terminal: termOpen ? { x: treeWidth, y: 1 + heights.code, width: codeWidth, height: termHeight } : null };
|
|
2308
|
-
const wheelFor = (pane) => (wheel?.pane === pane ? wheel : null);
|
|
2309
2348
|
const scrollFor = (pane) => (scrollTo?.pane === pane ? scrollTo : null);
|
|
2310
2349
|
const totals = diagnostics.counts();
|
|
2311
2350
|
const checking = diagnostics.busy;
|
|
@@ -2324,6 +2363,48 @@ ${snippet}
|
|
|
2324
2363
|
setRepaint((n) => n + 1);
|
|
2325
2364
|
}
|
|
2326
2365
|
/** /workspace: list them, `new [folder]` opens one, a number switches. */
|
|
2366
|
+
/**
|
|
2367
|
+
* + in the header (or /workspace new on its own): a picker of folders to open as a workspace: this project
|
|
2368
|
+
* again (a second workspace on the same code), then the projects beside it, latest first. Enter opens it.
|
|
2369
|
+
*/
|
|
2370
|
+
function pickWorkspaceFolder() {
|
|
2371
|
+
const here = process.cwd();
|
|
2372
|
+
const parent = path.dirname(here);
|
|
2373
|
+
const home = os.homedir();
|
|
2374
|
+
// Where each folder is, kept short (its end is what tells them apart) so the names keep their room.
|
|
2375
|
+
const short = (p) => {
|
|
2376
|
+
const shown = p.startsWith(home) ? `~${p.slice(home.length)}` : p;
|
|
2377
|
+
return shown.length > 22 ? `…${shown.slice(-21)}` : shown;
|
|
2378
|
+
};
|
|
2379
|
+
let beside = [];
|
|
2380
|
+
try {
|
|
2381
|
+
beside = readdirSync(parent, { withFileTypes: true })
|
|
2382
|
+
.filter((d) => d.isDirectory() && !d.name.startsWith(".") && path.join(parent, d.name) !== here)
|
|
2383
|
+
.map((d) => {
|
|
2384
|
+
const full = path.join(parent, d.name);
|
|
2385
|
+
let time = 0;
|
|
2386
|
+
try {
|
|
2387
|
+
time = statSync(full).mtimeMs;
|
|
2388
|
+
}
|
|
2389
|
+
catch {
|
|
2390
|
+
// unreadable: listed last
|
|
2391
|
+
}
|
|
2392
|
+
return { path: full, time };
|
|
2393
|
+
})
|
|
2394
|
+
.sort((a, b) => b.time - a.time)
|
|
2395
|
+
.slice(0, 60);
|
|
2396
|
+
}
|
|
2397
|
+
catch {
|
|
2398
|
+
// the parent can't be listed: just this project, or type a path with /workspace new <folder>
|
|
2399
|
+
}
|
|
2400
|
+
const open = (folder) => () => void workspaceCommand(`new ${folder}`);
|
|
2401
|
+
setResumePicker([
|
|
2402
|
+
{ title: `${path.basename(here)} (this project)`, keys: short(here), run: open(here) },
|
|
2403
|
+
...beside.map((b) => ({ title: path.basename(b.path), keys: short(b.path), run: open(b.path) })),
|
|
2404
|
+
]);
|
|
2405
|
+
setPickerTitle("Open a folder as a new workspace · any other: /workspace new <path>");
|
|
2406
|
+
setQuickOpen(">");
|
|
2407
|
+
}
|
|
2327
2408
|
async function workspaceCommand(arg) {
|
|
2328
2409
|
const [sub, ...more] = arg.split(/\s+/);
|
|
2329
2410
|
const tabs = workspaceTabs();
|
|
@@ -2341,6 +2422,8 @@ ${snippet}
|
|
|
2341
2422
|
if (sub !== "new")
|
|
2342
2423
|
return info("Try /workspace new <folder>, /workspace <n> or /workspace.", "error");
|
|
2343
2424
|
const wanted = more.join(" ");
|
|
2425
|
+
if (!wanted)
|
|
2426
|
+
return pickWorkspaceFolder();
|
|
2344
2427
|
if (isHosted())
|
|
2345
2428
|
return void sendToHost({ type: "new", folder: wanted });
|
|
2346
2429
|
const folder = workspaceFolder(wanted, process.cwd());
|
|
@@ -2500,7 +2583,7 @@ ${snippet}
|
|
|
2500
2583
|
? [["Enter", "open"], ["e", "edit"], ["/", "command"], ["c", "changes"], ["Ctrl+P", "find"], ["u", "undo"], ["p", "problems"], ["< >", "resize"], ["m", "select text"], ["s", "settings"]]
|
|
2501
2584
|
: diff
|
|
2502
2585
|
? [["Esc", "close diff"], ["n/N", "next/prev change"], ...(diffMode === "session" ? [["[ ]", "prev/next file"]] : []), ["v", "cycle diff"]]
|
|
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"]];
|
|
2586
|
+
: [["double-click/e", "edit"], [FIND_KEY, "find"], ["v", "diff"], ...(recent.length > 1 ? [["b", "back"]] : []), ["u", "undo"], ["/", "command"], ["p", "problems"], ["+ -", "zoom"], ["< >", "resize"], ["m", "select text"], ["s", "settings"]];
|
|
2504
2587
|
const followChip = follow ? ` ◉ follow${followLag ? ` ·${followLag}` : ""} ` : " ○ follow ";
|
|
2505
2588
|
const statusUsed = mode.label.length + 2 + autoMode.length + 2 + followChip.length + ` ✗ ${totals.errors} ▲ ${totals.warnings} `.length + (checking.length ? 4 + checking.join(", ").length : 0) + 2;
|
|
2506
2589
|
const shownHints = [];
|
|
@@ -2515,7 +2598,7 @@ ${snippet}
|
|
|
2515
2598
|
y: () => answer(true), n: approval ? () => answer(false) : undefined, o: openPending,
|
|
2516
2599
|
"Shift+Tab": () => cycleAutoMode(), "/": () => typeCommand("/"), "Ctrl+P": () => setQuickOpen(""),
|
|
2517
2600
|
Tab: () => setFocus((f) => FOCUS_ORDER[(FOCUS_ORDER.indexOf(f) + 1) % FOCUS_ORDER.length]),
|
|
2518
|
-
e: startEditing, "double-click/e": startEditing, b: goBack, [FIND_KEY]: showEditor ? undefined : startFind, v: cycleDiff, u: undoLast, p: toggleProblems, m: toggleMouse,
|
|
2601
|
+
"+ -": () => zoomCode(1), e: startEditing, "double-click/e": startEditing, b: goBack, [FIND_KEY]: showEditor ? undefined : startFind, v: cycleDiff, u: undoLast, p: toggleProblems, m: toggleMouse,
|
|
2519
2602
|
s: () => setView("settings"), "/help": () => void runCommand("/help"),
|
|
2520
2603
|
Esc: busy ? stop : diff && !showEditor ? () => setDiffMode("off") : undefined,
|
|
2521
2604
|
};
|
|
@@ -2541,9 +2624,9 @@ ${snippet}
|
|
|
2541
2624
|
}
|
|
2542
2625
|
const modelLabel = agentRef.current.id === "nou" ? describe(choice) : `${engineInfo(agentRef.current.id).label} · ${engineInfo(agentRef.current.id).model(choice)}`;
|
|
2543
2626
|
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,
|
|
2627
|
+
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, wheelPane: "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, wheelPane: "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, wheelPane: "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, wheelPane: "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, wheelPane: "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
|
|
2545
2628
|
? _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)` : ""] })
|
|
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",
|
|
2629
|
+
: 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", wheelPane: "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: "type to filter", title: pickerTitle } : {}), onPick: (file) => {
|
|
2547
2630
|
setQuickOpen(null);
|
|
2548
2631
|
clearFollow();
|
|
2549
2632
|
open(file);
|