@datalabrotterdam/nova-ai-cli 1.2.0-alpha.1 → 1.2.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +165 -0
- package/README.md +74 -32
- package/dist/browser-setup-7SWHUQEL.js +57 -0
- package/dist/chunk-FMHWW3E6.js +11 -0
- package/dist/{chunk-ZKRZIF3S.js → chunk-KDAUUQBE.js} +49 -360
- package/dist/chunk-L77KQRGZ.js +170 -0
- package/dist/{headless-ZYUSTZDL.js → headless-BQXK7TNC.js} +29 -46
- package/dist/index.js +57 -19
- package/dist/{tui-XOKXPXN2.js → tui-TJFV6NC6.js} +719 -335
- package/dist/update-check-G3PYCRWM.js +23 -0
- package/package.json +25 -39
- package/dist/acp-ZYOQ6PON.js +0 -65
- package/dist/chunk-GZZ7PY3S.js +0 -8878
- package/dist/chunk-JAEXUFAS.js +0 -451
- package/dist/chunk-Q7WL3DXP.js +0 -19
- package/dist/index.d.ts +0 -1
- package/dist/login-6U7QS7EA.js +0 -118
- package/dist/webui-L4GGMXJ3.js +0 -9
- package/src/acp/web/dist/assets/index-CSccI7lJ.css +0 -1
- package/src/acp/web/dist/assets/index-DlCz3fmq.js +0 -1
- package/src/acp/web/dist/favicon.svg +0 -46
- package/src/acp/web/dist/index.html +0 -32
|
@@ -1,52 +1,140 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
workspaceNeedsTrust
|
|
7
|
-
} from "./chunk-ZKRZIF3S.js";
|
|
8
|
-
import {
|
|
9
|
-
INTERACTION_MODES,
|
|
10
|
-
NovaAgent,
|
|
11
|
-
OTHER_OPTION_ID,
|
|
12
|
-
discoverSkills,
|
|
13
|
-
isInteractionMode,
|
|
14
|
-
listStoredSessions,
|
|
15
|
-
loadStoredSession,
|
|
16
|
-
replayHistory,
|
|
17
|
-
resolveModelContextWindow,
|
|
18
|
-
savePermissionMode,
|
|
19
|
-
savedPermissionMode
|
|
20
|
-
} from "./chunk-GZZ7PY3S.js";
|
|
2
|
+
checkForUpdate,
|
|
3
|
+
rememberUpdateAnswer,
|
|
4
|
+
shouldAskAboutUpdate
|
|
5
|
+
} from "./chunk-L77KQRGZ.js";
|
|
21
6
|
import {
|
|
22
|
-
|
|
23
|
-
|
|
7
|
+
CLI_PACKAGE_NAME,
|
|
8
|
+
cliVersion
|
|
9
|
+
} from "./chunk-FMHWW3E6.js";
|
|
24
10
|
import {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
resolveDefaultModel,
|
|
29
|
-
resolveModelSupportsImageInput,
|
|
30
|
-
saveDefaultModel,
|
|
31
|
-
setWorkspaceTrusted
|
|
32
|
-
} from "./chunk-JAEXUFAS.js";
|
|
11
|
+
TuiAcpClient,
|
|
12
|
+
createStore
|
|
13
|
+
} from "./chunk-KDAUUQBE.js";
|
|
33
14
|
|
|
34
15
|
// src/tui/index.ts
|
|
16
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
17
|
+
import { readCredentials } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
|
|
18
|
+
import { listStoredSessions as listStoredSessions2, loadStoredSession as loadStoredSession2 } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
|
|
19
|
+
import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
|
|
20
|
+
|
|
21
|
+
// src/update-install.ts
|
|
22
|
+
import { spawnSync } from "child_process";
|
|
23
|
+
import { existsSync, realpathSync } from "fs";
|
|
24
|
+
import { dirname, join, sep } from "path";
|
|
35
25
|
import { createInterface } from "readline/promises";
|
|
26
|
+
var START_WAIT_MS = 1500;
|
|
27
|
+
var UPDATED_FROM_ENV = "NOVA_UPDATED_FROM";
|
|
28
|
+
function globalNpmPrefix(options = {}) {
|
|
29
|
+
const script = options.scriptPath ?? process.argv[1];
|
|
30
|
+
if (!script) return null;
|
|
31
|
+
const windows = (options.platform ?? process.platform) === "win32";
|
|
32
|
+
const marker = `${sep}node_modules${sep}${CLI_PACKAGE_NAME.split("/").join(sep)}${sep}`;
|
|
33
|
+
const resolved = realPath(script);
|
|
34
|
+
const at = resolved.lastIndexOf(marker);
|
|
35
|
+
if (at < 0) return null;
|
|
36
|
+
const nodeModulesParent = resolved.slice(0, at);
|
|
37
|
+
const prefix = windows ? nodeModulesParent : dirname(nodeModulesParent);
|
|
38
|
+
if (!windows && nodeModulesParent !== join(prefix, "lib")) return null;
|
|
39
|
+
return existsSync(join(prefix, windows ? "nova-ai.cmd" : join("bin", "nova-ai"))) ? prefix : null;
|
|
40
|
+
}
|
|
41
|
+
async function withInstallability(update, options = {}) {
|
|
42
|
+
if (!update) return null;
|
|
43
|
+
return { ...update, installable: globalNpmPrefix(options) !== null };
|
|
44
|
+
}
|
|
45
|
+
function npmInvocation(args) {
|
|
46
|
+
return process.platform === "win32" ? [`npm ${args.map((arg) => /\s/.test(arg) ? `"${arg}"` : arg).join(" ")}`, [], true] : ["npm", args, false];
|
|
47
|
+
}
|
|
48
|
+
function realPath(path) {
|
|
49
|
+
try {
|
|
50
|
+
return realpathSync(path);
|
|
51
|
+
} catch {
|
|
52
|
+
return path;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function parseUpdateAnswer(answer) {
|
|
56
|
+
const value = answer.trim().toLowerCase();
|
|
57
|
+
if (value === "" || value === "y" || value === "yes") return "update";
|
|
58
|
+
if (value === "s" || value === "skip") return "skip";
|
|
59
|
+
return "later";
|
|
60
|
+
}
|
|
61
|
+
async function askToUpdate(update) {
|
|
62
|
+
const prompt = createInterface({ input: process.stdin, output: process.stdout });
|
|
63
|
+
try {
|
|
64
|
+
console.log(`
|
|
65
|
+
Update available: nova-ai ${update.currentVersion} \u2192 ${update.latestVersion}`);
|
|
66
|
+
return parseUpdateAnswer(
|
|
67
|
+
await prompt.question("Update now? [Y/n/s] (n: not today, s: skip this version) ")
|
|
68
|
+
);
|
|
69
|
+
} finally {
|
|
70
|
+
prompt.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function offerUpdateAtStart(check, restartArgs) {
|
|
74
|
+
let timer;
|
|
75
|
+
const update = await Promise.race([
|
|
76
|
+
check,
|
|
77
|
+
new Promise((resolve2) => {
|
|
78
|
+
timer = setTimeout(() => resolve2(null), START_WAIT_MS);
|
|
79
|
+
})
|
|
80
|
+
]);
|
|
81
|
+
clearTimeout(timer);
|
|
82
|
+
if (!update?.installable || !shouldAskAboutUpdate(update)) return null;
|
|
83
|
+
const answer = await askToUpdate(update);
|
|
84
|
+
if (answer !== "update") {
|
|
85
|
+
rememberUpdateAnswer(answer, update);
|
|
86
|
+
console.log(
|
|
87
|
+
answer === "skip" ? `Skipping ${update.latestVersion}; you'll be asked again for a newer version.` : "Not now. /update installs it any time."
|
|
88
|
+
);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return installAndRestart(update, restartArgs);
|
|
92
|
+
}
|
|
93
|
+
function installAndRestart(update, restartArgs) {
|
|
94
|
+
const prefix = globalNpmPrefix();
|
|
95
|
+
if (!prefix) return null;
|
|
96
|
+
console.log(`
|
|
97
|
+
Installing nova-ai ${update.latestVersion}\u2026`);
|
|
98
|
+
const [file, args, shell] = npmInvocation(["install", "-g", "--prefix", prefix, `${CLI_PACKAGE_NAME}@${update.tag}`]);
|
|
99
|
+
const install = spawnSync(file, args, { stdio: "inherit", shell });
|
|
100
|
+
if (install.status !== 0) {
|
|
101
|
+
console.error(`
|
|
102
|
+
The update did not install. Run it yourself: ${update.command}
|
|
103
|
+
`);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const script = process.argv[1];
|
|
107
|
+
if (!script) return null;
|
|
108
|
+
const restarted = spawnSync(process.execPath, [...process.execArgv, script, ...restartArgs], {
|
|
109
|
+
stdio: "inherit",
|
|
110
|
+
env: { ...process.env, [UPDATED_FROM_ENV]: update.currentVersion }
|
|
111
|
+
});
|
|
112
|
+
return restarted.status ?? 1;
|
|
113
|
+
}
|
|
36
114
|
|
|
37
115
|
// src/tui/ink/app.tsx
|
|
38
|
-
import { Box as Box6, render,
|
|
39
|
-
import { useEffect, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
116
|
+
import { Box as Box6, render, Text as Text6, useBoxMetrics, useInput as useInput3, useWindowSize } from "ink";
|
|
117
|
+
import { memo, useEffect, useRef, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
|
|
118
|
+
import { savedPermissionMode } from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
|
|
40
119
|
|
|
41
120
|
// src/tui/session/session-runner.ts
|
|
42
121
|
import * as acp from "@agentclientprotocol/sdk";
|
|
122
|
+
import { NovaAgent } from "@datalabrotterdam/nova-ai-agent/acp/agent.js";
|
|
123
|
+
import { startInProcessAgent } from "@datalabrotterdam/nova-ai-agent/client/in-process.js";
|
|
124
|
+
import { replayHistory } from "@datalabrotterdam/nova-ai-agent/core/history.js";
|
|
125
|
+
import {
|
|
126
|
+
saveDefaultEffort,
|
|
127
|
+
saveDefaultModel
|
|
128
|
+
} from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
|
|
43
129
|
import {
|
|
44
|
-
|
|
45
|
-
} from "@datalabrotterdam/nova-
|
|
130
|
+
savePermissionMode
|
|
131
|
+
} from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
|
|
132
|
+
import { loadStoredSession } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
|
|
133
|
+
import { resolveEffort } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
|
|
46
134
|
|
|
47
135
|
// src/tui/files/file-mentions.ts
|
|
48
136
|
import { statSync } from "fs";
|
|
49
|
-
import { isAbsolute, relative, resolve, sep } from "path";
|
|
137
|
+
import { isAbsolute, relative, resolve, sep as sep2 } from "path";
|
|
50
138
|
var MENTION_RE = /@(?:"([^"]+)"|'([^']+)'|([^\s]+))/g;
|
|
51
139
|
var TRAILING_PUNCTUATION = /[.,;:!?\)\]\}]+$/;
|
|
52
140
|
function findWorkspaceFileMentions(text, cwd) {
|
|
@@ -56,10 +144,10 @@ function findWorkspaceFileMentions(text, cwd) {
|
|
|
56
144
|
const raw = match[1] ?? match[2] ?? match[3] ?? "";
|
|
57
145
|
const candidates = [raw, raw.replace(TRAILING_PUNCTUATION, "")].filter((value, index, all) => value && all.indexOf(value) === index);
|
|
58
146
|
for (const candidate of candidates) {
|
|
59
|
-
const normalized = candidate.replace(/[\\/]/g,
|
|
147
|
+
const normalized = candidate.replace(/[\\/]/g, sep2);
|
|
60
148
|
const absolutePath = resolve(workspaceRoot, normalized);
|
|
61
149
|
const relativePath = relative(workspaceRoot, absolutePath);
|
|
62
|
-
if (relativePath.startsWith(`..${
|
|
150
|
+
if (relativePath.startsWith(`..${sep2}`) || relativePath === ".." || isAbsolute(relativePath)) continue;
|
|
63
151
|
try {
|
|
64
152
|
if (!statSync(absolutePath).isFile()) continue;
|
|
65
153
|
} catch {
|
|
@@ -67,7 +155,7 @@ function findWorkspaceFileMentions(text, cwd) {
|
|
|
67
155
|
}
|
|
68
156
|
mentions.set(absolutePath.toLowerCase(), {
|
|
69
157
|
absolutePath,
|
|
70
|
-
relativePath: relativePath.split(
|
|
158
|
+
relativePath: relativePath.split(sep2).join("/")
|
|
71
159
|
});
|
|
72
160
|
break;
|
|
73
161
|
}
|
|
@@ -187,15 +275,6 @@ var SessionRunner = class {
|
|
|
187
275
|
constructor(store, credentials, cwd) {
|
|
188
276
|
this.store = store;
|
|
189
277
|
this.credentials = credentials;
|
|
190
|
-
this.loadMcpConfiguration(cwd);
|
|
191
|
-
this.skills = discoverSkills(cwd).map(
|
|
192
|
-
({ name, description, source, path }) => ({
|
|
193
|
-
name,
|
|
194
|
-
description,
|
|
195
|
-
source,
|
|
196
|
-
path
|
|
197
|
-
})
|
|
198
|
-
);
|
|
199
278
|
this.cwd = cwd;
|
|
200
279
|
this.acpClient = new TuiAcpClient(
|
|
201
280
|
store,
|
|
@@ -210,13 +289,8 @@ var SessionRunner = class {
|
|
|
210
289
|
clientCapabilities: this.acpClient.capabilities,
|
|
211
290
|
clientInfo: { name: "nova-ai-cli-tui", version: "1" }
|
|
212
291
|
});
|
|
213
|
-
this.
|
|
214
|
-
|
|
215
|
-
if (!model)
|
|
216
|
-
throw new Error(
|
|
217
|
-
"No Nova model configured. Re-run authentication or set NOVA_MODEL."
|
|
218
|
-
);
|
|
219
|
-
this.model = model;
|
|
292
|
+
this.model = credentials.defaultModel ?? process.env.NOVA_MODEL ?? "";
|
|
293
|
+
this.effort = credentials.defaultEffort ?? null;
|
|
220
294
|
this.sessionId = crypto.randomUUID();
|
|
221
295
|
this.sessionReady = Promise.resolve();
|
|
222
296
|
}
|
|
@@ -227,21 +301,26 @@ var SessionRunner = class {
|
|
|
227
301
|
title = null;
|
|
228
302
|
model;
|
|
229
303
|
interactionMode = "agent";
|
|
304
|
+
/** The modes the agent offers: the built-in ones, then the user's agent files. */
|
|
305
|
+
get modeIds() {
|
|
306
|
+
return this.acpClient.modeIds;
|
|
307
|
+
}
|
|
308
|
+
/** The agent's slash commands (the user's command files); sent as a prompt. */
|
|
309
|
+
get agentCommands() {
|
|
310
|
+
return this.acpClient.agentCommands;
|
|
311
|
+
}
|
|
230
312
|
/** The agent, served in-process over a real ACP connection (tests may stub it). */
|
|
231
313
|
agent = new NovaAgent();
|
|
232
314
|
acpClient;
|
|
233
315
|
connection;
|
|
234
316
|
nova;
|
|
235
317
|
initialized;
|
|
236
|
-
novaClient;
|
|
237
|
-
mcpServers = [];
|
|
238
|
-
mcpConfigurationFailures = [];
|
|
239
318
|
mcpStatus = {
|
|
240
319
|
configured: [],
|
|
241
320
|
connected: [],
|
|
242
321
|
failures: []
|
|
243
322
|
};
|
|
244
|
-
skills;
|
|
323
|
+
skills = [];
|
|
245
324
|
sessionReady;
|
|
246
325
|
agentSessionLoaded = false;
|
|
247
326
|
promptActive = false;
|
|
@@ -249,10 +328,10 @@ var SessionRunner = class {
|
|
|
249
328
|
handoffActive = false;
|
|
250
329
|
queuedAttachments = /* @__PURE__ */ new Map();
|
|
251
330
|
editingQueuedMessageId = null;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
331
|
+
/** From the agent's model list: image input, thinking levels. */
|
|
332
|
+
modelInfo = /* @__PURE__ */ new Map();
|
|
333
|
+
/** The thinking level the user chose (saved); null: each model's default. */
|
|
334
|
+
effort;
|
|
256
335
|
contextUsageRequest = 0;
|
|
257
336
|
modelCheck = null;
|
|
258
337
|
resumeFrom(storedSessionId) {
|
|
@@ -265,15 +344,7 @@ var SessionRunner = class {
|
|
|
265
344
|
this.sessionId = stored.sessionId;
|
|
266
345
|
this.cwd = stored.cwd;
|
|
267
346
|
this.title = stored.title;
|
|
268
|
-
this.
|
|
269
|
-
this.skills = discoverSkills(stored.cwd).map(
|
|
270
|
-
({ name, description, source, path }) => ({
|
|
271
|
-
name,
|
|
272
|
-
description,
|
|
273
|
-
source,
|
|
274
|
-
path
|
|
275
|
-
})
|
|
276
|
-
);
|
|
347
|
+
this.skills = [];
|
|
277
348
|
const messages = restoreSessionMessages(stored.messages);
|
|
278
349
|
this.store.setState({
|
|
279
350
|
messages,
|
|
@@ -292,12 +363,13 @@ var SessionRunner = class {
|
|
|
292
363
|
return await this.nova.loadSession({
|
|
293
364
|
sessionId: stored.sessionId,
|
|
294
365
|
cwd: stored.cwd,
|
|
295
|
-
mcpServers:
|
|
366
|
+
mcpServers: []
|
|
296
367
|
});
|
|
297
368
|
} finally {
|
|
298
369
|
this.acpClient.muteSessionUpdates = false;
|
|
299
370
|
}
|
|
300
371
|
}).then((response) => {
|
|
372
|
+
this.applyModes(response.modes);
|
|
301
373
|
this.applyMcpStatus(response._meta);
|
|
302
374
|
this.applySkillStatus(response._meta);
|
|
303
375
|
this.agentSessionLoaded = true;
|
|
@@ -305,6 +377,7 @@ var SessionRunner = class {
|
|
|
305
377
|
void this.nova.setSessionMode({ sessionId: this.sessionId, modeId: this.interactionMode }).catch(() => {
|
|
306
378
|
});
|
|
307
379
|
this.applyPermissionMode(this.sessionId);
|
|
380
|
+
this.applyEffort(this.sessionId);
|
|
308
381
|
void this.updateContextUsage();
|
|
309
382
|
void this.refreshContextUsage().catch(() => {
|
|
310
383
|
});
|
|
@@ -417,9 +490,6 @@ var SessionRunner = class {
|
|
|
417
490
|
void this.enqueueQueuedPrompt(value, [], [], "steer", true);
|
|
418
491
|
return true;
|
|
419
492
|
}
|
|
420
|
-
configuredMcpServers() {
|
|
421
|
-
return [...this.mcpServers];
|
|
422
|
-
}
|
|
423
493
|
mcpSessionStatus() {
|
|
424
494
|
return {
|
|
425
495
|
configured: this.mcpStatus.configured.map((server) => ({ ...server })),
|
|
@@ -430,27 +500,37 @@ var SessionRunner = class {
|
|
|
430
500
|
skillSessionStatus() {
|
|
431
501
|
return this.skills.map((skill) => ({ ...skill }));
|
|
432
502
|
}
|
|
503
|
+
/** Signs in to (or out of) an HTTP MCP server; the agent reconnects it and reports the new status. */
|
|
504
|
+
async mcpSignIn(serverName, signIn) {
|
|
505
|
+
await this.ensureSession();
|
|
506
|
+
const params = { sessionId: this.sessionId, serverName };
|
|
507
|
+
const meta = signIn ? await this.nova.mcpLogin(params) : await this.nova.mcpLogout(params);
|
|
508
|
+
this.applyMcpStatus(meta);
|
|
509
|
+
return this.mcpSessionStatus();
|
|
510
|
+
}
|
|
511
|
+
async trustWorkspace() {
|
|
512
|
+
await this.initialized;
|
|
513
|
+
await this.nova.workspaceTrust({ cwd: this.cwd, trusted: true });
|
|
514
|
+
}
|
|
515
|
+
/** Every skill of the workspace with its on/off state, from the agent. */
|
|
516
|
+
async listSkills() {
|
|
517
|
+
await this.initialized;
|
|
518
|
+
return (await this.nova.skillsList({ cwd: this.cwd })).skills;
|
|
519
|
+
}
|
|
520
|
+
async setSkillEnabled(name, enabled, global) {
|
|
521
|
+
await this.initialized;
|
|
522
|
+
return (await this.nova.skillsSetEnabled({
|
|
523
|
+
cwd: this.cwd,
|
|
524
|
+
name,
|
|
525
|
+
enabled,
|
|
526
|
+
scope: global ? "global" : "project"
|
|
527
|
+
})).skills;
|
|
528
|
+
}
|
|
433
529
|
contextUsageSnapshot() {
|
|
434
530
|
return this.store.getState().contextUsage;
|
|
435
531
|
}
|
|
436
532
|
async refreshContextUsage() {
|
|
437
533
|
await this.ensureSession();
|
|
438
|
-
const sessionId = this.sessionId;
|
|
439
|
-
const model = this.model;
|
|
440
|
-
if (this.contextWindowModel !== model) {
|
|
441
|
-
const request = ++this.contextWindowRequest;
|
|
442
|
-
const contextWindow = await resolveModelContextWindow(
|
|
443
|
-
this.novaClient,
|
|
444
|
-
model
|
|
445
|
-
);
|
|
446
|
-
if (request === this.contextWindowRequest && model === this.model) {
|
|
447
|
-
this.contextWindow = contextWindow;
|
|
448
|
-
this.contextWindowModel = model;
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
if (sessionId !== this.sessionId || !this.agentSessionLoaded) {
|
|
452
|
-
return this.refreshContextUsage();
|
|
453
|
-
}
|
|
454
534
|
const usage = await this.updateContextUsage();
|
|
455
535
|
if (!usage)
|
|
456
536
|
throw new Error(
|
|
@@ -466,13 +546,8 @@ var SessionRunner = class {
|
|
|
466
546
|
}
|
|
467
547
|
setModel(model) {
|
|
468
548
|
this.model = model;
|
|
469
|
-
this.contextWindow = null;
|
|
470
|
-
this.contextWindowModel = null;
|
|
471
|
-
this.contextWindowRequest++;
|
|
472
549
|
saveDefaultModel(model);
|
|
473
550
|
if (this.agentSessionLoaded) void this.updateContextUsage();
|
|
474
|
-
void this.refreshContextUsage().catch(() => {
|
|
475
|
-
});
|
|
476
551
|
}
|
|
477
552
|
/**
|
|
478
553
|
* Hands the mode to the agent's permission policy (which decides) and
|
|
@@ -530,27 +605,34 @@ var SessionRunner = class {
|
|
|
530
605
|
}
|
|
531
606
|
/** Chat models only: embedding, speech and transcription models can't run a session. */
|
|
532
607
|
async listModels() {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
608
|
+
await this.initialized;
|
|
609
|
+
const { models } = await this.nova.modelsList();
|
|
610
|
+
for (const model of models) this.modelInfo.set(model.id, model);
|
|
611
|
+
return models.map((model) => ({ id: model.id, name: model.name, thinking: model.thinking }));
|
|
612
|
+
}
|
|
613
|
+
/** Sync, from the loaded model list: false until it has loaded. */
|
|
614
|
+
hasThinkingLevels(model = this.model) {
|
|
615
|
+
return (this.modelInfo.get(model)?.efforts.length ?? 0) > 0;
|
|
538
616
|
}
|
|
539
617
|
/**
|
|
540
|
-
*
|
|
541
|
-
*
|
|
618
|
+
* Takes the account's default model when none is set, and replaces a saved
|
|
619
|
+
* one that cannot chat (older logins saved the first model of the list,
|
|
620
|
+
* e.g. an embedding model); the agent decides, the choice is saved.
|
|
542
621
|
* Runs once; a failed lookup keeps the model and the prompt reports errors.
|
|
543
622
|
*/
|
|
544
623
|
ensureUsableModel() {
|
|
545
624
|
this.modelCheck ??= (async () => {
|
|
546
625
|
try {
|
|
547
|
-
|
|
548
|
-
const
|
|
549
|
-
|
|
626
|
+
await this.initialized;
|
|
627
|
+
const { models, defaultModel } = await this.nova.modelsList(
|
|
628
|
+
this.model ? { preferred: this.model } : {}
|
|
629
|
+
);
|
|
630
|
+
for (const model of models) this.modelInfo.set(model.id, model);
|
|
631
|
+
if (!defaultModel || defaultModel === this.model) return;
|
|
550
632
|
const previous = this.model;
|
|
551
|
-
this.setModel(
|
|
633
|
+
this.setModel(defaultModel);
|
|
552
634
|
this.store.setState({
|
|
553
|
-
statusLine: `${previous} is not a chat model; switched to ${
|
|
635
|
+
statusLine: previous ? `${previous} is not a chat model; switched to ${defaultModel} (Ctrl+P to pick another).` : `Using model ${defaultModel} (Ctrl+P switches).`
|
|
554
636
|
});
|
|
555
637
|
} catch {
|
|
556
638
|
}
|
|
@@ -558,14 +640,41 @@ var SessionRunner = class {
|
|
|
558
640
|
return this.modelCheck;
|
|
559
641
|
}
|
|
560
642
|
async supportsImageInput(model = this.model) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
);
|
|
567
|
-
|
|
568
|
-
|
|
643
|
+
return (await this.modelDetails(model))?.imageInput ?? false;
|
|
644
|
+
}
|
|
645
|
+
/** What the agent's model list says about `model`; undefined when it isn't listed. */
|
|
646
|
+
async modelDetails(model = this.model) {
|
|
647
|
+
if (!this.modelInfo.has(model)) await this.listModels();
|
|
648
|
+
return this.modelInfo.get(model);
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* The level the current model thinks at, as the agent picks it (the
|
|
652
|
+
* chosen one when the model has it); null for a model without levels
|
|
653
|
+
* or before the model list has loaded.
|
|
654
|
+
*/
|
|
655
|
+
currentEffort() {
|
|
656
|
+
const info = this.modelInfo.get(this.model);
|
|
657
|
+
if (!info?.efforts.length) return null;
|
|
658
|
+
return resolveEffort(
|
|
659
|
+
{ reasoning_efforts: info.efforts, reasoning_default: info.reasoningDefault },
|
|
660
|
+
this.effort
|
|
661
|
+
) ?? null;
|
|
662
|
+
}
|
|
663
|
+
/** The current model's levels from the loaded list; empty when it has none. */
|
|
664
|
+
modelEffortLevels() {
|
|
665
|
+
return this.modelInfo.get(this.model)?.efforts ?? [];
|
|
666
|
+
}
|
|
667
|
+
/** Saved and kept across models and sessions; a model without the level uses its default. */
|
|
668
|
+
setEffort(level) {
|
|
669
|
+
this.effort = level;
|
|
670
|
+
saveDefaultEffort(level);
|
|
671
|
+
if (this.agentSessionLoaded) this.applyEffort(this.sessionId);
|
|
672
|
+
}
|
|
673
|
+
applyEffort(sessionId) {
|
|
674
|
+
if (!this.effort) return;
|
|
675
|
+
void this.nova.setSessionConfigOption({ sessionId, configId: "thought_level", value: this.effort }).catch(() => {
|
|
676
|
+
this.acpClient.appendError("Could not change the thinking level.");
|
|
677
|
+
});
|
|
569
678
|
}
|
|
570
679
|
async compactContext() {
|
|
571
680
|
if (this.store.getState().busy || this.promptActive) {
|
|
@@ -689,7 +798,7 @@ var SessionRunner = class {
|
|
|
689
798
|
});
|
|
690
799
|
}
|
|
691
800
|
} catch (err) {
|
|
692
|
-
const message = err instanceof
|
|
801
|
+
const message = err instanceof Error ? err.message : "Unknown error.";
|
|
693
802
|
pendingToolFailure = `Request failed: ${message}`;
|
|
694
803
|
this.acpClient.appendError(message);
|
|
695
804
|
this.store.setState({ statusLine: "Request failed." });
|
|
@@ -843,13 +952,13 @@ var SessionRunner = class {
|
|
|
843
952
|
];
|
|
844
953
|
}
|
|
845
954
|
async createSession() {
|
|
846
|
-
this.loadMcpConfiguration(this.cwd);
|
|
847
955
|
await this.initialized;
|
|
848
|
-
const { sessionId, _meta } = await this.nova.newSession({
|
|
956
|
+
const { sessionId, _meta, modes } = await this.nova.newSession({
|
|
849
957
|
cwd: this.cwd,
|
|
850
|
-
mcpServers:
|
|
958
|
+
mcpServers: []
|
|
851
959
|
});
|
|
852
960
|
this.sessionId = sessionId;
|
|
961
|
+
this.applyModes(modes);
|
|
853
962
|
this.applyMcpStatus(_meta);
|
|
854
963
|
this.applySkillStatus(_meta);
|
|
855
964
|
this.agentSessionLoaded = true;
|
|
@@ -857,6 +966,7 @@ var SessionRunner = class {
|
|
|
857
966
|
void this.nova.setSessionMode({ sessionId, modeId: this.interactionMode }).catch(() => {
|
|
858
967
|
});
|
|
859
968
|
this.applyPermissionMode(sessionId);
|
|
969
|
+
this.applyEffort(sessionId);
|
|
860
970
|
void this.updateContextUsage();
|
|
861
971
|
void this.refreshContextUsage().catch(() => {
|
|
862
972
|
});
|
|
@@ -871,10 +981,20 @@ var SessionRunner = class {
|
|
|
871
981
|
const value = meta?.["nova-ai-cli/mcp"];
|
|
872
982
|
if (!value || typeof value !== "object") return;
|
|
873
983
|
const record = value;
|
|
984
|
+
const strings = (value2) => Array.isArray(value2) ? value2.filter((entry) => typeof entry === "string") : void 0;
|
|
874
985
|
const configured = Array.isArray(record.configured) ? record.configured.flatMap((entry) => {
|
|
875
986
|
if (!entry || typeof entry !== "object") return [];
|
|
876
987
|
const item = entry;
|
|
877
|
-
return typeof item.name === "string" && typeof item.transport === "string" ? [
|
|
988
|
+
return typeof item.name === "string" && typeof item.transport === "string" ? [
|
|
989
|
+
{
|
|
990
|
+
name: item.name,
|
|
991
|
+
transport: item.transport,
|
|
992
|
+
...typeof item.url === "string" ? { url: item.url } : {},
|
|
993
|
+
...typeof item.command === "string" ? { command: item.command } : {},
|
|
994
|
+
...strings(item.args) ? { args: strings(item.args) } : {},
|
|
995
|
+
...strings(item.env) ? { env: strings(item.env) } : {}
|
|
996
|
+
}
|
|
997
|
+
] : [];
|
|
878
998
|
}) : [];
|
|
879
999
|
const connected = Array.isArray(record.connected) ? record.connected.filter(
|
|
880
1000
|
(entry) => typeof entry === "string"
|
|
@@ -882,26 +1002,15 @@ var SessionRunner = class {
|
|
|
882
1002
|
const failures = Array.isArray(record.failures) ? record.failures.flatMap((entry) => {
|
|
883
1003
|
if (!entry || typeof entry !== "object") return [];
|
|
884
1004
|
const item = entry;
|
|
885
|
-
return typeof item.serverName === "string" && typeof item.message === "string" ? [
|
|
1005
|
+
return typeof item.serverName === "string" && typeof item.message === "string" ? [
|
|
1006
|
+
{
|
|
1007
|
+
serverName: item.serverName,
|
|
1008
|
+
message: item.message,
|
|
1009
|
+
...item.needsAuth === true ? { needsAuth: true } : {}
|
|
1010
|
+
}
|
|
1011
|
+
] : [];
|
|
886
1012
|
}) : [];
|
|
887
|
-
this.mcpStatus = {
|
|
888
|
-
configured,
|
|
889
|
-
connected,
|
|
890
|
-
failures: [...this.mcpConfigurationFailures, ...failures]
|
|
891
|
-
};
|
|
892
|
-
}
|
|
893
|
-
loadMcpConfiguration(cwd) {
|
|
894
|
-
const configuration = readWorkspaceMcpConfiguration(cwd);
|
|
895
|
-
this.mcpServers = configuration.servers;
|
|
896
|
-
this.mcpConfigurationFailures = configuration.failures;
|
|
897
|
-
this.mcpStatus = {
|
|
898
|
-
configured: this.mcpServers.map((server) => ({
|
|
899
|
-
name: server.name,
|
|
900
|
-
transport: "type" in server ? server.type : "stdio"
|
|
901
|
-
})),
|
|
902
|
-
connected: [],
|
|
903
|
-
failures: [...this.mcpConfigurationFailures]
|
|
904
|
-
};
|
|
1013
|
+
this.mcpStatus = { configured, connected, failures };
|
|
905
1014
|
}
|
|
906
1015
|
applySkillStatus(meta) {
|
|
907
1016
|
const value = meta?.["nova-ai-cli/skills"];
|
|
@@ -919,14 +1028,34 @@ var SessionRunner = class {
|
|
|
919
1028
|
] : [];
|
|
920
1029
|
});
|
|
921
1030
|
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Starts the session ahead of the first prompt, so the agent's MCP and
|
|
1033
|
+
* skill status is there for /mcp and the status line. A failure is left to
|
|
1034
|
+
* the first prompt, which tries again and reports it.
|
|
1035
|
+
*/
|
|
1036
|
+
async prepareSession() {
|
|
1037
|
+
try {
|
|
1038
|
+
await this.ensureSession();
|
|
1039
|
+
} catch {
|
|
1040
|
+
this.sessionReady = Promise.resolve();
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
get sessionLoaded() {
|
|
1044
|
+
return this.agentSessionLoaded;
|
|
1045
|
+
}
|
|
922
1046
|
async ensureSession() {
|
|
923
1047
|
await this.ensureUsableModel();
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1048
|
+
for (; ; ) {
|
|
1049
|
+
const ready = this.sessionReady;
|
|
1050
|
+
await ready;
|
|
1051
|
+
if (this.agentSessionLoaded) return;
|
|
1052
|
+
if (this.sessionReady === ready) this.sessionReady = this.createSession();
|
|
928
1053
|
}
|
|
929
1054
|
}
|
|
1055
|
+
applyModes(modes) {
|
|
1056
|
+
if (modes?.availableModes.length) this.acpClient.modeIds = modes.availableModes.map((mode) => mode.id);
|
|
1057
|
+
if (!this.modeIds.includes(this.interactionMode)) this.applyInteractionMode("agent", "Mode: agent");
|
|
1058
|
+
}
|
|
930
1059
|
applyInteractionMode(mode, statusLine) {
|
|
931
1060
|
this.interactionMode = mode;
|
|
932
1061
|
this.store.setState({ interactionMode: mode, statusLine });
|
|
@@ -940,7 +1069,7 @@ var SessionRunner = class {
|
|
|
940
1069
|
try {
|
|
941
1070
|
const usage = await this.nova.sessionContextUsage({
|
|
942
1071
|
sessionId,
|
|
943
|
-
|
|
1072
|
+
...this.model ? { model: this.model } : {},
|
|
944
1073
|
mode: this.interactionMode
|
|
945
1074
|
});
|
|
946
1075
|
if (request === this.contextUsageRequest && sessionId === this.sessionId) {
|
|
@@ -1013,82 +1142,6 @@ function fromAgentPermissionMode(mode) {
|
|
|
1013
1142
|
return mode === "bypassPermissions" ? "bypassAll" : mode === "acceptEdits" ? "acceptEdits" : "ask";
|
|
1014
1143
|
}
|
|
1015
1144
|
|
|
1016
|
-
// src/tui/update-check.ts
|
|
1017
|
-
var PACKAGE_NAME = "@datalabrotterdam/nova-ai-cli";
|
|
1018
|
-
var REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME.replace("/", "%2F")}/latest`;
|
|
1019
|
-
var DEFAULT_TIMEOUT_MS = 3e3;
|
|
1020
|
-
var UPDATE_COMMAND = "npm install -g @datalabrotterdam/nova-ai-cli@latest";
|
|
1021
|
-
async function checkForUpdate(options = {}) {
|
|
1022
|
-
try {
|
|
1023
|
-
const currentVersion = options.currentVersion ?? readInstalledVersion();
|
|
1024
|
-
if (!currentVersion) return null;
|
|
1025
|
-
const response = await (options.fetchImpl ?? fetch)(REGISTRY_URL, {
|
|
1026
|
-
headers: { accept: "application/json" },
|
|
1027
|
-
signal: AbortSignal.timeout(options.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
1028
|
-
});
|
|
1029
|
-
if (!response.ok) return null;
|
|
1030
|
-
const payload = await response.json();
|
|
1031
|
-
if (!isVersionPayload(payload)) return null;
|
|
1032
|
-
if (!isNewerVersion(payload.version, currentVersion)) return null;
|
|
1033
|
-
return {
|
|
1034
|
-
currentVersion,
|
|
1035
|
-
latestVersion: payload.version,
|
|
1036
|
-
command: UPDATE_COMMAND
|
|
1037
|
-
};
|
|
1038
|
-
} catch {
|
|
1039
|
-
return null;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
function readInstalledVersion() {
|
|
1043
|
-
return readPackageVersion();
|
|
1044
|
-
}
|
|
1045
|
-
function isNewerVersion(candidate, current) {
|
|
1046
|
-
const next = parseVersion(candidate);
|
|
1047
|
-
const installed = parseVersion(current);
|
|
1048
|
-
if (!next || !installed) return false;
|
|
1049
|
-
for (let index = 0; index < 3; index++) {
|
|
1050
|
-
const difference = next.core[index] - installed.core[index];
|
|
1051
|
-
if (difference !== 0) return difference > 0;
|
|
1052
|
-
}
|
|
1053
|
-
return comparePrerelease(next.prerelease, installed.prerelease) > 0;
|
|
1054
|
-
}
|
|
1055
|
-
function isVersionPayload(value) {
|
|
1056
|
-
return typeof value === "object" && value !== null && "version" in value && typeof value.version === "string";
|
|
1057
|
-
}
|
|
1058
|
-
function parseVersion(value) {
|
|
1059
|
-
const match = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(
|
|
1060
|
-
value
|
|
1061
|
-
);
|
|
1062
|
-
if (!match) return null;
|
|
1063
|
-
return {
|
|
1064
|
-
core: [Number(match[1]), Number(match[2]), Number(match[3])],
|
|
1065
|
-
prerelease: match[4]?.split(".") ?? []
|
|
1066
|
-
};
|
|
1067
|
-
}
|
|
1068
|
-
function comparePrerelease(left, right) {
|
|
1069
|
-
if (left.length === 0 || right.length === 0) {
|
|
1070
|
-
if (left.length === right.length) return 0;
|
|
1071
|
-
return left.length === 0 ? 1 : -1;
|
|
1072
|
-
}
|
|
1073
|
-
const length = Math.max(left.length, right.length);
|
|
1074
|
-
for (let index = 0; index < length; index++) {
|
|
1075
|
-
const leftPart = left[index];
|
|
1076
|
-
const rightPart = right[index];
|
|
1077
|
-
if (leftPart === void 0 || rightPart === void 0) {
|
|
1078
|
-
return leftPart === rightPart ? 0 : leftPart === void 0 ? -1 : 1;
|
|
1079
|
-
}
|
|
1080
|
-
if (leftPart === rightPart) continue;
|
|
1081
|
-
const leftNumber = /^\d+$/.test(leftPart) ? Number(leftPart) : null;
|
|
1082
|
-
const rightNumber = /^\d+$/.test(rightPart) ? Number(rightPart) : null;
|
|
1083
|
-
if (leftNumber !== null && rightNumber !== null)
|
|
1084
|
-
return leftNumber - rightNumber;
|
|
1085
|
-
if (leftNumber !== null || rightNumber !== null)
|
|
1086
|
-
return leftNumber !== null ? -1 : 1;
|
|
1087
|
-
return leftPart < rightPart ? -1 : 1;
|
|
1088
|
-
}
|
|
1089
|
-
return 0;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
1145
|
// src/tui/ink/terminal-keyboard.ts
|
|
1093
1146
|
var KITTY_QUERY = "\x1B[?u";
|
|
1094
1147
|
var DEVICE_ATTRIBUTES_QUERY = "\x1B[c";
|
|
@@ -1130,6 +1183,17 @@ function kittyKeyboardOverride(env = process.env) {
|
|
|
1130
1183
|
function isLateTerminalReply(input) {
|
|
1131
1184
|
return /^\u001b?\[\?[\d;]*[uc]$/.test(input);
|
|
1132
1185
|
}
|
|
1186
|
+
var MOUSE_REPORTING_ON = "\x1B[?1000h\x1B[?1006h";
|
|
1187
|
+
var MOUSE_REPORTING_OFF = "\x1B[?1000l\x1B[?1006l";
|
|
1188
|
+
var MOUSE_INPUT = /^\u001b?\[<(\d+);\d+;\d+[Mm]$/;
|
|
1189
|
+
function isMouseInput(input) {
|
|
1190
|
+
return MOUSE_INPUT.test(input);
|
|
1191
|
+
}
|
|
1192
|
+
function wheelRows(input) {
|
|
1193
|
+
const button = Number(MOUSE_INPUT.exec(input)?.[1] ?? -1);
|
|
1194
|
+
if (button < 0 || (button & 64) === 0) return 0;
|
|
1195
|
+
return button & 1 ? -3 : 3;
|
|
1196
|
+
}
|
|
1133
1197
|
|
|
1134
1198
|
// src/tui/ink/components/prompt-input.tsx
|
|
1135
1199
|
import { Box, Text, useInput, usePaste } from "ink";
|
|
@@ -1147,7 +1211,8 @@ var palette = {
|
|
|
1147
1211
|
faint: "#64748b",
|
|
1148
1212
|
code: "#fcd34d",
|
|
1149
1213
|
diffAddBg: "#1b3a24",
|
|
1150
|
-
diffRemoveBg: "#3d1f1f"
|
|
1214
|
+
diffRemoveBg: "#3d1f1f",
|
|
1215
|
+
stickyBackground: "#1e293b"
|
|
1151
1216
|
};
|
|
1152
1217
|
var colors = {
|
|
1153
1218
|
primary: chalk.hex(palette.primary),
|
|
@@ -1174,7 +1239,7 @@ function PromptInput({
|
|
|
1174
1239
|
}) {
|
|
1175
1240
|
useSyncExternalStore(editor.subscribe, editor.getVersion);
|
|
1176
1241
|
useInput((input, key) => {
|
|
1177
|
-
if (onKey(input, key)) return;
|
|
1242
|
+
if (isMouseInput(input) || onKey(input, key)) return;
|
|
1178
1243
|
editor.handleKey(input, key);
|
|
1179
1244
|
});
|
|
1180
1245
|
usePaste((text) => editor.paste(text));
|
|
@@ -1460,15 +1525,18 @@ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-run
|
|
|
1460
1525
|
function StatusLine({
|
|
1461
1526
|
ui,
|
|
1462
1527
|
model,
|
|
1463
|
-
|
|
1528
|
+
effort = null,
|
|
1529
|
+
mcp,
|
|
1530
|
+
scroll = 0
|
|
1464
1531
|
}) {
|
|
1465
1532
|
const separator = /* @__PURE__ */ jsx2(Text2, { color: palette.faint, children: " | " });
|
|
1466
1533
|
const usage = ui.contextUsage ? `ctx:${formatTokenCount(ui.contextUsage.totalTokens)}${ui.contextUsage.contextWindow ? `/${formatTokenCount(ui.contextUsage.contextWindow)}` : ""}` : null;
|
|
1467
1534
|
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
1468
|
-
ui.updateAvailable ? /* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: `Update ${ui.updateAvailable.latestVersion} available (current ${ui.updateAvailable.currentVersion}). Run: ${ui.updateAvailable.command}` }) : null,
|
|
1535
|
+
ui.updateAvailable ? /* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: `Update ${ui.updateAvailable.latestVersion} available (current ${ui.updateAvailable.currentVersion}). ${ui.updateAvailable.installable ? "Type /update to install it." : `Run: ${ui.updateAvailable.command}`}` }) : null,
|
|
1469
1536
|
ui.statusLine ? /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: ui.statusLine }) : null,
|
|
1470
1537
|
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
|
|
1471
1538
|
/* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: model }),
|
|
1539
|
+
effort ? /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: ` \xB7 thinking ${effort.toLowerCase()}` }) : null,
|
|
1472
1540
|
separator,
|
|
1473
1541
|
ui.interactionMode,
|
|
1474
1542
|
separator,
|
|
@@ -1488,6 +1556,10 @@ function StatusLine({
|
|
|
1488
1556
|
`mcp:${mcp.connected}/${mcp.configured}`,
|
|
1489
1557
|
mcp.failed ? /* @__PURE__ */ jsx2(Text2, { color: palette.danger, children: ` !${mcp.failed}` }) : null
|
|
1490
1558
|
] }) : null,
|
|
1559
|
+
scroll ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1560
|
+
separator,
|
|
1561
|
+
/* @__PURE__ */ jsx2(Text2, { color: palette.warning, children: `\u2191${scroll} lines \xB7 PgDn/Esc` })
|
|
1562
|
+
] }) : null,
|
|
1491
1563
|
mcp.skills ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
|
|
1492
1564
|
separator,
|
|
1493
1565
|
`skills:${mcp.skills}`
|
|
@@ -1814,7 +1886,7 @@ var builtinCommands = [
|
|
|
1814
1886
|
name: "help",
|
|
1815
1887
|
description: "Show available commands",
|
|
1816
1888
|
run(ctx) {
|
|
1817
|
-
const lines = builtinCommands.map((c) => `/${c.name} \u2014 ${c.description}`);
|
|
1889
|
+
const lines = [...builtinCommands.filter((c) => c.available?.(ctx) ?? true), ...ctx.agentCommands()].map((c) => `/${c.name} \u2014 ${c.description}`);
|
|
1818
1890
|
ctx.print(lines.join("\n"));
|
|
1819
1891
|
}
|
|
1820
1892
|
},
|
|
@@ -1880,22 +1952,30 @@ var builtinCommands = [
|
|
|
1880
1952
|
},
|
|
1881
1953
|
{
|
|
1882
1954
|
name: "exit",
|
|
1883
|
-
description: "Exit nova-ai
|
|
1955
|
+
description: "Exit nova-ai",
|
|
1884
1956
|
run(ctx) {
|
|
1885
1957
|
ctx.exit();
|
|
1886
1958
|
}
|
|
1887
1959
|
},
|
|
1960
|
+
{
|
|
1961
|
+
name: "update",
|
|
1962
|
+
description: "Install the newer nova-ai from npm and restart in this session",
|
|
1963
|
+
run(ctx) {
|
|
1964
|
+
const message = ctx.requestUpdate();
|
|
1965
|
+
if (message) ctx.print(message);
|
|
1966
|
+
}
|
|
1967
|
+
},
|
|
1888
1968
|
{
|
|
1889
1969
|
name: "mode",
|
|
1890
|
-
description: "Switch interaction mode (/mode agent|ask|plan)",
|
|
1970
|
+
description: "Switch interaction mode (/mode agent|ask|plan|<agent file>)",
|
|
1891
1971
|
run(ctx, args) {
|
|
1892
1972
|
const mode = args.trim();
|
|
1893
1973
|
if (!mode) {
|
|
1894
|
-
ctx.print(`Current mode: ${ctx.getInteractionMode()}`);
|
|
1974
|
+
ctx.print(`Current mode: ${ctx.getInteractionMode()} (available: ${ctx.getInteractionModes().join(", ")})`);
|
|
1895
1975
|
return;
|
|
1896
1976
|
}
|
|
1897
|
-
if (!
|
|
1898
|
-
ctx.print(
|
|
1977
|
+
if (!ctx.getInteractionModes().includes(mode)) {
|
|
1978
|
+
ctx.print(`Usage: /mode ${ctx.getInteractionModes().join("|")}`);
|
|
1899
1979
|
return;
|
|
1900
1980
|
}
|
|
1901
1981
|
ctx.setInteractionMode(mode);
|
|
@@ -1920,8 +2000,8 @@ var builtinCommands = [
|
|
|
1920
2000
|
{
|
|
1921
2001
|
name: "trust",
|
|
1922
2002
|
description: "Trust this workspace: start the MCP servers it declares and apply its .nova-ai allow rules",
|
|
1923
|
-
run(ctx) {
|
|
1924
|
-
ctx.print(ctx.trustWorkspace());
|
|
2003
|
+
async run(ctx) {
|
|
2004
|
+
ctx.print(await ctx.trustWorkspace());
|
|
1925
2005
|
}
|
|
1926
2006
|
},
|
|
1927
2007
|
{
|
|
@@ -1937,6 +2017,16 @@ var builtinCommands = [
|
|
|
1937
2017
|
ctx.openModelPicker();
|
|
1938
2018
|
}
|
|
1939
2019
|
},
|
|
2020
|
+
{
|
|
2021
|
+
name: "effort",
|
|
2022
|
+
description: "Set how hard the model thinks (/effort <level>, or no args for a slider)",
|
|
2023
|
+
available: (ctx) => ctx.hasThinkingLevels(),
|
|
2024
|
+
async run(ctx, args) {
|
|
2025
|
+
const level = args.trim();
|
|
2026
|
+
if (level) ctx.print(await ctx.setEffort(level));
|
|
2027
|
+
else await ctx.openEffortPicker();
|
|
2028
|
+
}
|
|
2029
|
+
},
|
|
1940
2030
|
{
|
|
1941
2031
|
name: "tools",
|
|
1942
2032
|
description: "Inspect tool calls from this session",
|
|
@@ -1946,16 +2036,38 @@ var builtinCommands = [
|
|
|
1946
2036
|
},
|
|
1947
2037
|
{
|
|
1948
2038
|
name: "mcp",
|
|
1949
|
-
description: "Inspect
|
|
1950
|
-
run(ctx) {
|
|
1951
|
-
|
|
2039
|
+
description: "Inspect MCP servers, or sign in to one: /mcp login|logout <server>",
|
|
2040
|
+
async run(ctx, args) {
|
|
2041
|
+
const [action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
2042
|
+
const name = rest.join(" ");
|
|
2043
|
+
if (!action) {
|
|
2044
|
+
await ctx.openMcpInspector();
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
if (action !== "login" && action !== "logout" || !name) {
|
|
2048
|
+
ctx.print("Usage: /mcp, or /mcp login|logout <server>");
|
|
2049
|
+
return;
|
|
2050
|
+
}
|
|
2051
|
+
if (action === "login") ctx.print(`Signing in to "${name}"\u2026 (waits up to 5 minutes for the browser)`);
|
|
2052
|
+
ctx.print(await ctx.mcpSignIn(name, action === "login"));
|
|
1952
2053
|
}
|
|
1953
2054
|
},
|
|
1954
2055
|
{
|
|
1955
2056
|
name: "skills",
|
|
1956
|
-
description: "
|
|
1957
|
-
run(ctx) {
|
|
1958
|
-
|
|
2057
|
+
description: "List skills, or switch one: /skills on|off <name> [--global]",
|
|
2058
|
+
async run(ctx, args) {
|
|
2059
|
+
const words = args.trim().split(/\s+/).filter(Boolean);
|
|
2060
|
+
if (!words.length) {
|
|
2061
|
+
await ctx.openSkillInspector();
|
|
2062
|
+
return;
|
|
2063
|
+
}
|
|
2064
|
+
const global = words.includes("--global");
|
|
2065
|
+
const [action, name] = words.filter((word) => word !== "--global");
|
|
2066
|
+
if (action !== "on" && action !== "off" || !name) {
|
|
2067
|
+
ctx.print("Usage: /skills, or /skills on|off <name> [--global]");
|
|
2068
|
+
return;
|
|
2069
|
+
}
|
|
2070
|
+
ctx.print(await ctx.switchSkill(name, action === "on", global));
|
|
1959
2071
|
}
|
|
1960
2072
|
},
|
|
1961
2073
|
{
|
|
@@ -2124,8 +2236,8 @@ function formatJob(job) {
|
|
|
2124
2236
|
|
|
2125
2237
|
// src/tui/commands/index.ts
|
|
2126
2238
|
var COMMANDS = [...builtinCommands];
|
|
2127
|
-
function allCommands() {
|
|
2128
|
-
return COMMANDS;
|
|
2239
|
+
function allCommands(ctx) {
|
|
2240
|
+
return ctx ? COMMANDS.filter((c) => c.available?.(ctx) ?? true) : COMMANDS;
|
|
2129
2241
|
}
|
|
2130
2242
|
function findCommand(name) {
|
|
2131
2243
|
return COMMANDS.find((c) => c.name === name);
|
|
@@ -2253,10 +2365,10 @@ console.log(ObjC.unwrap(png.base64EncodedStringWithOptions(0)));
|
|
|
2253
2365
|
`;
|
|
2254
2366
|
|
|
2255
2367
|
// src/tui/files/find-fd.ts
|
|
2256
|
-
import { spawnSync } from "child_process";
|
|
2368
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2257
2369
|
var FD_CANDIDATES = ["fd", "fdfind"];
|
|
2258
2370
|
var cachedPath;
|
|
2259
|
-
function resolveFdPath(run = (command, args) =>
|
|
2371
|
+
function resolveFdPath(run = (command, args) => spawnSync2(command, args, { stdio: "ignore" })) {
|
|
2260
2372
|
if (cachedPath !== void 0) return cachedPath;
|
|
2261
2373
|
for (const candidate of FD_CANDIDATES) {
|
|
2262
2374
|
const result = run(candidate, ["--version"]);
|
|
@@ -2271,7 +2383,7 @@ function resolveFdPath(run = (command, args) => spawnSync(command, args, { stdio
|
|
|
2271
2383
|
|
|
2272
2384
|
// src/tui/files/list-workspace-files.ts
|
|
2273
2385
|
import { readdirSync } from "fs";
|
|
2274
|
-
import { join, relative as relative2, sep as
|
|
2386
|
+
import { join as join2, relative as relative2, sep as sep3 } from "path";
|
|
2275
2387
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".nova-ai", "out", "coverage", ".next", ".turbo"]);
|
|
2276
2388
|
function listWorkspaceFiles(cwd, max = 2e3) {
|
|
2277
2389
|
const results = [];
|
|
@@ -2287,12 +2399,12 @@ function listWorkspaceFiles(cwd, max = 2e3) {
|
|
|
2287
2399
|
for (const entry of entries) {
|
|
2288
2400
|
if (results.length >= max) break;
|
|
2289
2401
|
if (entry.isSymbolicLink()) continue;
|
|
2290
|
-
const full =
|
|
2402
|
+
const full = join2(dir, entry.name);
|
|
2291
2403
|
if (entry.isDirectory()) {
|
|
2292
2404
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
2293
2405
|
stack.push(full);
|
|
2294
2406
|
} else if (entry.isFile()) {
|
|
2295
|
-
results.push(relative2(cwd, full).split(
|
|
2407
|
+
results.push(relative2(cwd, full).split(sep3).join("/"));
|
|
2296
2408
|
}
|
|
2297
2409
|
}
|
|
2298
2410
|
}
|
|
@@ -2323,7 +2435,7 @@ var DraftImageAttachments = class {
|
|
|
2323
2435
|
import { spawn } from "child_process";
|
|
2324
2436
|
import { readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
2325
2437
|
import { homedir } from "os";
|
|
2326
|
-
import { basename, dirname, join as
|
|
2438
|
+
import { basename, dirname as dirname2, join as join3 } from "path";
|
|
2327
2439
|
var PATH_DELIMITERS = /* @__PURE__ */ new Set([" ", " ", '"', "'", "="]);
|
|
2328
2440
|
var MAX_SUGGESTIONS2 = 20;
|
|
2329
2441
|
var WorkspaceAutocompleteProvider = class {
|
|
@@ -2343,7 +2455,8 @@ var WorkspaceAutocompleteProvider = class {
|
|
|
2343
2455
|
const mention = extractMentionPrefix(before);
|
|
2344
2456
|
if (mention) return this.mentionSuggestions(mention, options.signal);
|
|
2345
2457
|
if (cursorLine === 0 && before.startsWith("/") && !/\s/.test(before)) {
|
|
2346
|
-
const
|
|
2458
|
+
const commands = typeof this.commands === "function" ? this.commands() : this.commands;
|
|
2459
|
+
const items2 = filterCommands(commands, before.slice(1));
|
|
2347
2460
|
return items2.length ? { items: items2, prefix: before } : null;
|
|
2348
2461
|
}
|
|
2349
2462
|
if (!options.force) return null;
|
|
@@ -2404,14 +2517,14 @@ var WorkspaceAutocompleteProvider = class {
|
|
|
2404
2517
|
pathSuggestions(prefix) {
|
|
2405
2518
|
const quoted = prefix.startsWith('"');
|
|
2406
2519
|
const raw = quoted ? prefix.slice(1) : prefix;
|
|
2407
|
-
const expanded = raw === "~" || raw.startsWith("~/") ?
|
|
2520
|
+
const expanded = raw === "~" || raw.startsWith("~/") ? join3(homedir(), raw.slice(1)) : raw;
|
|
2408
2521
|
const absolute = expanded.startsWith("/") || /^[A-Za-z]:[\\/]/.test(expanded);
|
|
2409
2522
|
const separator = Math.max(raw.lastIndexOf("/"), raw.lastIndexOf("\\"));
|
|
2410
2523
|
const listsDirectory = raw === "" || raw === "~" || separator === raw.length - 1;
|
|
2411
|
-
const directoryPart = listsDirectory ? expanded :
|
|
2524
|
+
const directoryPart = listsDirectory ? expanded : dirname2(expanded);
|
|
2412
2525
|
const namePrefix = listsDirectory ? "" : basename(expanded);
|
|
2413
2526
|
const displayDirectory = raw === "~" ? "~/" : separator >= 0 ? raw.slice(0, separator + 1) : "";
|
|
2414
|
-
const searchDirectory = absolute ? directoryPart :
|
|
2527
|
+
const searchDirectory = absolute ? directoryPart : join3(this.cwd, directoryPart);
|
|
2415
2528
|
let entries;
|
|
2416
2529
|
try {
|
|
2417
2530
|
entries = readdirSync2(searchDirectory, { withFileTypes: true });
|
|
@@ -2425,7 +2538,7 @@ var WorkspaceAutocompleteProvider = class {
|
|
|
2425
2538
|
if (!isDirectory && entry.isSymbolicLink()) {
|
|
2426
2539
|
try {
|
|
2427
2540
|
isDirectory = statSync2(
|
|
2428
|
-
|
|
2541
|
+
join3(searchDirectory, entry.name)
|
|
2429
2542
|
).isDirectory();
|
|
2430
2543
|
} catch {
|
|
2431
2544
|
}
|
|
@@ -2561,6 +2674,7 @@ function walkWithFd(cwd, fdPath, query, signal) {
|
|
|
2561
2674
|
}
|
|
2562
2675
|
|
|
2563
2676
|
// src/tui/session/session-picker.ts
|
|
2677
|
+
import { listStoredSessions } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
|
|
2564
2678
|
function listSessionsForCwd(cwd) {
|
|
2565
2679
|
return listStoredSessions(cwd);
|
|
2566
2680
|
}
|
|
@@ -2609,6 +2723,9 @@ function formatDateTime(updatedAt) {
|
|
|
2609
2723
|
return Number.isFinite(timestamp) ? new Date(timestamp).toISOString() : "unknown";
|
|
2610
2724
|
}
|
|
2611
2725
|
|
|
2726
|
+
// src/tui/ink/dialogs/state.ts
|
|
2727
|
+
import { OTHER_OPTION_ID } from "@datalabrotterdam/nova-ai-agent/core/user-questions.js";
|
|
2728
|
+
|
|
2612
2729
|
// src/tui/ink/editor/buffer.ts
|
|
2613
2730
|
var segmenter2 = new Intl.Segmenter(void 0, { granularity: "grapheme" });
|
|
2614
2731
|
function boundaries(text) {
|
|
@@ -2862,6 +2979,40 @@ var SelectionState = class {
|
|
|
2862
2979
|
return "changed";
|
|
2863
2980
|
}
|
|
2864
2981
|
};
|
|
2982
|
+
var SliderState = class {
|
|
2983
|
+
constructor(levels, selected, onSelect, onCancel) {
|
|
2984
|
+
this.levels = levels;
|
|
2985
|
+
this.selected = selected;
|
|
2986
|
+
this.onSelect = onSelect;
|
|
2987
|
+
this.onCancel = onCancel;
|
|
2988
|
+
}
|
|
2989
|
+
levels;
|
|
2990
|
+
selected;
|
|
2991
|
+
onSelect;
|
|
2992
|
+
onCancel;
|
|
2993
|
+
handleKey(input, key) {
|
|
2994
|
+
if (isCancel(input, key)) {
|
|
2995
|
+
this.onCancel();
|
|
2996
|
+
return "closed";
|
|
2997
|
+
}
|
|
2998
|
+
if (key.return) {
|
|
2999
|
+
const level = this.levels[this.selected];
|
|
3000
|
+
if (!level) return "ignored";
|
|
3001
|
+
this.onSelect(level);
|
|
3002
|
+
return "closed";
|
|
3003
|
+
}
|
|
3004
|
+
const last = this.levels.length - 1;
|
|
3005
|
+
let next = this.selected;
|
|
3006
|
+
if (key.leftArrow || key.downArrow) next = Math.max(0, this.selected - 1);
|
|
3007
|
+
else if (key.rightArrow || key.upArrow) next = Math.min(last, this.selected + 1);
|
|
3008
|
+
else if (key.home) next = 0;
|
|
3009
|
+
else if (key.end) next = last;
|
|
3010
|
+
else return "ignored";
|
|
3011
|
+
if (next === this.selected) return "ignored";
|
|
3012
|
+
this.selected = next;
|
|
3013
|
+
return "changed";
|
|
3014
|
+
}
|
|
3015
|
+
};
|
|
2865
3016
|
var ScrollState = class {
|
|
2866
3017
|
constructor(onClose) {
|
|
2867
3018
|
this.onClose = onClose;
|
|
@@ -3110,6 +3261,9 @@ function permissionKey(request, input, key) {
|
|
|
3110
3261
|
return "closed";
|
|
3111
3262
|
}
|
|
3112
3263
|
|
|
3264
|
+
// src/tui/ink/controller.ts
|
|
3265
|
+
import { effortLabel } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
|
|
3266
|
+
|
|
3113
3267
|
// src/tui/ink/editor/prompt-editor.ts
|
|
3114
3268
|
var MAX_HISTORY = 200;
|
|
3115
3269
|
var PromptEditor = class {
|
|
@@ -3545,17 +3699,11 @@ var TranscriptCommitter = class {
|
|
|
3545
3699
|
planSignature = null;
|
|
3546
3700
|
snapshot = null;
|
|
3547
3701
|
lastState = null;
|
|
3548
|
-
redrawRequested = false;
|
|
3549
|
-
/** The next sync starts a new epoch, e.g. after the terminal width changed. */
|
|
3550
|
-
requestRedraw() {
|
|
3551
|
-
this.redrawRequested = true;
|
|
3552
|
-
this.lastState = null;
|
|
3553
|
-
}
|
|
3554
3702
|
sync(state, expanded) {
|
|
3555
3703
|
if (this.snapshot && state === this.lastState && expanded === this.expanded)
|
|
3556
3704
|
return this.snapshot;
|
|
3557
3705
|
this.lastState = state;
|
|
3558
|
-
if (
|
|
3706
|
+
if (state.sessionId !== this.sessionId || expanded !== this.expanded || !this.prefixIntact(state.messages)) {
|
|
3559
3707
|
this.startEpoch(state, expanded);
|
|
3560
3708
|
}
|
|
3561
3709
|
const messages = state.messages;
|
|
@@ -3708,7 +3856,6 @@ var TranscriptCommitter = class {
|
|
|
3708
3856
|
return true;
|
|
3709
3857
|
}
|
|
3710
3858
|
startEpoch(state, expanded) {
|
|
3711
|
-
this.redrawRequested = false;
|
|
3712
3859
|
this.epoch++;
|
|
3713
3860
|
this.sessionId = state.sessionId;
|
|
3714
3861
|
this.expanded = expanded;
|
|
@@ -3754,7 +3901,6 @@ function safeSplitOffset(text, from) {
|
|
|
3754
3901
|
}
|
|
3755
3902
|
|
|
3756
3903
|
// src/tui/ink/controller.ts
|
|
3757
|
-
var CLEAR_SCREEN_AND_SCROLLBACK = "\x1B[2J\x1B[3J\x1B[H";
|
|
3758
3904
|
var EXIT_CONFIRM_MS = 2e3;
|
|
3759
3905
|
var TuiController = class {
|
|
3760
3906
|
constructor(store, runner, io) {
|
|
@@ -3779,6 +3925,8 @@ var TuiController = class {
|
|
|
3779
3925
|
exiting = false;
|
|
3780
3926
|
view;
|
|
3781
3927
|
lastEpoch = 0;
|
|
3928
|
+
scroll = 0;
|
|
3929
|
+
viewport = { rows: 10, contentRows: 0 };
|
|
3782
3930
|
exitArmed = false;
|
|
3783
3931
|
exitTimer;
|
|
3784
3932
|
animationTimer;
|
|
@@ -3790,6 +3938,8 @@ var TuiController = class {
|
|
|
3790
3938
|
unsubscribeStore;
|
|
3791
3939
|
resolveExit = () => {
|
|
3792
3940
|
};
|
|
3941
|
+
/** Set by /update: the caller installs and restarts after the TUI exits. */
|
|
3942
|
+
updateRequested = false;
|
|
3793
3943
|
/** Settles once the user asked to quit; `run` then tears the view down. */
|
|
3794
3944
|
exitRequested = new Promise((resolve2) => {
|
|
3795
3945
|
this.resolveExit = resolve2;
|
|
@@ -3800,7 +3950,7 @@ var TuiController = class {
|
|
|
3800
3950
|
};
|
|
3801
3951
|
getView = () => this.view;
|
|
3802
3952
|
start() {
|
|
3803
|
-
void this.runner.
|
|
3953
|
+
void this.runner.prepareSession().then(() => this.refresh());
|
|
3804
3954
|
this.syncWindowTitle();
|
|
3805
3955
|
this.animationTimer = setInterval(() => this.syncWindowTitle(true), 120);
|
|
3806
3956
|
this.animationTimer.unref?.();
|
|
@@ -3835,8 +3985,17 @@ var TuiController = class {
|
|
|
3835
3985
|
this.runner.cancel();
|
|
3836
3986
|
return true;
|
|
3837
3987
|
}
|
|
3988
|
+
if (this.scroll > 0) {
|
|
3989
|
+
this.scrollTo(0);
|
|
3990
|
+
return true;
|
|
3991
|
+
}
|
|
3838
3992
|
return false;
|
|
3839
3993
|
}
|
|
3994
|
+
if (key.pageUp || key.pageDown) {
|
|
3995
|
+
const page = Math.max(1, this.viewport.rows - 2);
|
|
3996
|
+
this.scrollBy(key.pageUp ? page : -page);
|
|
3997
|
+
return true;
|
|
3998
|
+
}
|
|
3840
3999
|
if (key.tab && key.shift) {
|
|
3841
4000
|
this.cycleInteractionMode();
|
|
3842
4001
|
return true;
|
|
@@ -3938,8 +4097,8 @@ var TuiController = class {
|
|
|
3938
4097
|
"Models",
|
|
3939
4098
|
models.map((model) => ({
|
|
3940
4099
|
value: model.id,
|
|
3941
|
-
label: model.id,
|
|
3942
|
-
description: model.name
|
|
4100
|
+
label: model.thinking ? `${model.id} \u25C6` : model.id,
|
|
4101
|
+
description: [model.name, model.thinking ? "thinking" : null].filter(Boolean).join(" \xB7 ") || void 0
|
|
3943
4102
|
})),
|
|
3944
4103
|
"model-picker",
|
|
3945
4104
|
(model) => this.setModel(model)
|
|
@@ -3948,6 +4107,58 @@ var TuiController = class {
|
|
|
3948
4107
|
this.showText("Models", errorMessage(error, "Failed to list models."));
|
|
3949
4108
|
}
|
|
3950
4109
|
};
|
|
4110
|
+
effortName() {
|
|
4111
|
+
const level = this.runner.currentEffort();
|
|
4112
|
+
const efforts = level ? this.runner.modelEffortLevels() : [];
|
|
4113
|
+
return level ? effortLabel(level, efforts) : null;
|
|
4114
|
+
}
|
|
4115
|
+
/** The selected model's levels and current one, or why it has none. */
|
|
4116
|
+
async effortLevels() {
|
|
4117
|
+
let info;
|
|
4118
|
+
try {
|
|
4119
|
+
info = await this.runner.modelDetails();
|
|
4120
|
+
} catch (error) {
|
|
4121
|
+
return errorMessage(error, "Could not load the model list.");
|
|
4122
|
+
}
|
|
4123
|
+
const model = this.runner.model || "This model";
|
|
4124
|
+
if (!info?.efforts.length) {
|
|
4125
|
+
return info?.thinking ? `${model} thinks at a fixed depth; there is no level to choose.` : `${model} is not a thinking model; /model lists the ones marked \u25C6.`;
|
|
4126
|
+
}
|
|
4127
|
+
return { efforts: info.efforts, current: this.runner.currentEffort() ?? info.efforts[0] };
|
|
4128
|
+
}
|
|
4129
|
+
/** /effort: a slider over the selected model's levels. */
|
|
4130
|
+
openEffort = async () => {
|
|
4131
|
+
const levels = await this.effortLevels();
|
|
4132
|
+
if (typeof levels === "string") {
|
|
4133
|
+
this.showText("Thinking effort", levels);
|
|
4134
|
+
return;
|
|
4135
|
+
}
|
|
4136
|
+
const state = new SliderState(
|
|
4137
|
+
levels.efforts.map((value) => ({ value, label: effortLabel(value, levels.efforts) })),
|
|
4138
|
+
Math.max(0, levels.efforts.indexOf(levels.current)),
|
|
4139
|
+
(item) => {
|
|
4140
|
+
this.closeOverlay();
|
|
4141
|
+
this.runner.setEffort(item.value);
|
|
4142
|
+
this.store.setState({ statusLine: `Thinking effort: ${item.label}.` });
|
|
4143
|
+
},
|
|
4144
|
+
this.closeOverlay
|
|
4145
|
+
);
|
|
4146
|
+
this.showOverlay({ kind: "slider", title: `Thinking effort \xB7 ${this.runner.model}`, state });
|
|
4147
|
+
};
|
|
4148
|
+
/** /effort <level>: by id ("high") or name ("Extra high", "off"). */
|
|
4149
|
+
setEffort = async (level) => {
|
|
4150
|
+
const levels = await this.effortLevels();
|
|
4151
|
+
if (typeof levels === "string") return levels;
|
|
4152
|
+
const wanted = level.trim().toLowerCase();
|
|
4153
|
+
const value = levels.efforts.find(
|
|
4154
|
+
(candidate) => candidate === wanted || effortLabel(candidate, levels.efforts).toLowerCase() === wanted
|
|
4155
|
+
);
|
|
4156
|
+
const names = levels.efforts.map((candidate) => effortLabel(candidate, levels.efforts).toLowerCase());
|
|
4157
|
+
if (!value) return `${this.runner.model} has these levels: ${names.join(", ")}.`;
|
|
4158
|
+
this.runner.setEffort(value);
|
|
4159
|
+
this.refresh();
|
|
4160
|
+
return `Thinking effort: ${effortLabel(value, levels.efforts)}.`;
|
|
4161
|
+
};
|
|
3951
4162
|
openPermissionModes = () => {
|
|
3952
4163
|
const modes = [
|
|
3953
4164
|
{ value: "ask", label: "ask", description: "Prompt before tools make changes" },
|
|
@@ -4001,9 +4212,13 @@ ${result.output}${result.truncated ? "\n\n[output truncated]" : ""}`
|
|
|
4001
4212
|
state: new ToolInspectorState(() => this.toolCalls(), this.closeOverlay)
|
|
4002
4213
|
});
|
|
4003
4214
|
};
|
|
4004
|
-
openMcpInspector = () => {
|
|
4005
|
-
|
|
4215
|
+
openMcpInspector = async () => {
|
|
4216
|
+
if (!this.runner.sessionLoaded) {
|
|
4217
|
+
this.showText("MCP servers", "Connecting MCP servers...");
|
|
4218
|
+
await this.runner.prepareSession();
|
|
4219
|
+
}
|
|
4006
4220
|
const connection = this.runner.mcpSessionStatus();
|
|
4221
|
+
const servers = connection.configured;
|
|
4007
4222
|
const configuredNames = new Set(servers.map((server) => server.name));
|
|
4008
4223
|
const configurationFailures = connection.failures.filter((failure) => !configuredNames.has(failure.serverName)).map((failure) => `${failure.serverName}
|
|
4009
4224
|
status: failed - ${failure.message}`);
|
|
@@ -4016,15 +4231,40 @@ status: failed - ${failure.message}`);
|
|
|
4016
4231
|
rows.length ? rows.join("\n\n") : "No MCP servers configured for this workspace.\n\nAdd a project .mcp.json or mcpServers to .nova-ai/settings.json; ACP connects them when the session starts."
|
|
4017
4232
|
);
|
|
4018
4233
|
};
|
|
4019
|
-
|
|
4020
|
-
|
|
4234
|
+
/** Every discovered skill with its state; switched-off skills stay listed. */
|
|
4235
|
+
openSkillInspector = async () => {
|
|
4236
|
+
let skills;
|
|
4237
|
+
try {
|
|
4238
|
+
skills = await this.runner.listSkills();
|
|
4239
|
+
} catch (error) {
|
|
4240
|
+
this.showText("Skills", errorMessage(error, "Could not list the skills."));
|
|
4241
|
+
return;
|
|
4242
|
+
}
|
|
4243
|
+
const on = skills.filter((skill) => skill.enabled).length;
|
|
4021
4244
|
this.showText(
|
|
4022
|
-
`Skills (${skills.length})`,
|
|
4023
|
-
skills.length ?
|
|
4245
|
+
`Skills (${on} on, ${skills.length - on} off)`,
|
|
4246
|
+
skills.length ? [
|
|
4247
|
+
...skills.map(
|
|
4248
|
+
(skill) => `${skill.name} [${skill.bundledBy ? `bundled with ${skill.bundledBy}` : skill.source}]${skill.enabled ? "" : " (off)"}
|
|
4024
4249
|
${skill.description}
|
|
4025
|
-
${skill.path}`
|
|
4250
|
+
${skill.path}`
|
|
4251
|
+
),
|
|
4252
|
+
"/skills off <name> switches one off in this project (--global: everywhere); /skills on <name> switches it back on."
|
|
4253
|
+
].join("\n\n") : "No skills discovered. Add SKILL.md under .nova-ai/skills (or .agents, .claude, .codex skills) in the workspace, or ~/.nova-ai/skills for all projects."
|
|
4026
4254
|
);
|
|
4027
4255
|
};
|
|
4256
|
+
/** /skills on|off <name> [--global]: applies from the next message. */
|
|
4257
|
+
switchSkill = async (name, enabled, global) => {
|
|
4258
|
+
let skills;
|
|
4259
|
+
try {
|
|
4260
|
+
skills = await this.runner.setSkillEnabled(name, enabled, global);
|
|
4261
|
+
} catch (error) {
|
|
4262
|
+
const message = errorMessage(error, "Could not change the skill.");
|
|
4263
|
+
return message.startsWith("No skill named") ? `${message} /skills lists them.` : message;
|
|
4264
|
+
}
|
|
4265
|
+
const stillOff = enabled && skills.some((skill) => skill.name === name && !skill.enabled);
|
|
4266
|
+
return stillOff ? `Switched "${name}" on ${global ? "globally" : "in this project"}, but it is still off ${global ? "in this project" : "globally"} (use ${global ? "/skills on " + name : "/skills on " + name + " --global"}).` : `Skill "${name}" is ${enabled ? "on" : "off"} ${global ? "everywhere" : "in this project"} from the next message.`;
|
|
4267
|
+
};
|
|
4028
4268
|
openUsageInspector = async () => {
|
|
4029
4269
|
this.showText("Context usage", "Calculating context usage...");
|
|
4030
4270
|
try {
|
|
@@ -4035,14 +4275,23 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
|
|
|
4035
4275
|
}
|
|
4036
4276
|
};
|
|
4037
4277
|
/**
|
|
4038
|
-
*
|
|
4039
|
-
*
|
|
4040
|
-
* erased exactly (Ink leaves stale prompt borders), and old scrollback keeps
|
|
4041
|
-
* the old wrapping.
|
|
4278
|
+
* The view reports its transcript size after each layout. While scrolled
|
|
4279
|
+
* up, new output below grows the offset so the rows being read stay put.
|
|
4042
4280
|
*/
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
this.
|
|
4281
|
+
setViewport(rows, contentRows) {
|
|
4282
|
+
const grown = contentRows - this.viewport.contentRows;
|
|
4283
|
+
this.viewport = { rows, contentRows };
|
|
4284
|
+
this.scrollTo(this.scroll > 0 && grown > 0 ? this.scroll + grown : this.scroll);
|
|
4285
|
+
}
|
|
4286
|
+
/** Positive scrolls toward older output. */
|
|
4287
|
+
scrollBy(rows) {
|
|
4288
|
+
this.scrollTo(this.scroll + rows);
|
|
4289
|
+
}
|
|
4290
|
+
scrollTo(offset) {
|
|
4291
|
+
const max = Math.max(0, this.viewport.contentRows - this.viewport.rows);
|
|
4292
|
+
const next = Math.max(0, Math.min(offset, max));
|
|
4293
|
+
if (next === this.scroll) return;
|
|
4294
|
+
this.scroll = next;
|
|
4046
4295
|
this.refresh();
|
|
4047
4296
|
}
|
|
4048
4297
|
toggleToolDetails() {
|
|
@@ -4067,8 +4316,8 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
|
|
|
4067
4316
|
this.runner.setInteractionMode(mode);
|
|
4068
4317
|
}
|
|
4069
4318
|
cycleInteractionMode() {
|
|
4070
|
-
const
|
|
4071
|
-
this.setInteractionMode(
|
|
4319
|
+
const modes = this.runner.modeIds;
|
|
4320
|
+
this.setInteractionMode(modes[(modes.indexOf(this.runner.interactionMode) + 1) % modes.length]);
|
|
4072
4321
|
}
|
|
4073
4322
|
appendAssistant(text) {
|
|
4074
4323
|
this.store.setState((state) => ({
|
|
@@ -4091,26 +4340,60 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
|
|
|
4091
4340
|
clear: () => void this.runner.startNewSession(),
|
|
4092
4341
|
newSession: () => this.runner.startNewSession(),
|
|
4093
4342
|
exit: () => this.requestExit(),
|
|
4343
|
+
requestUpdate: () => {
|
|
4344
|
+
const update = this.store.getState().updateAvailable;
|
|
4345
|
+
if (!update) return "No newer nova-ai found on npm.";
|
|
4346
|
+
if (!update.installable) {
|
|
4347
|
+
return `nova-ai ${update.latestVersion} is available, but this copy wasn't installed with npm -g, so it can't update itself. Run: ${update.command}`;
|
|
4348
|
+
}
|
|
4349
|
+
if (this.store.getState().busy) {
|
|
4350
|
+
return "Wait for the current request to finish (or press Esc), then run /update again.";
|
|
4351
|
+
}
|
|
4352
|
+
this.updateRequested = true;
|
|
4353
|
+
this.requestExit();
|
|
4354
|
+
return null;
|
|
4355
|
+
},
|
|
4094
4356
|
resumeSession: (sessionId) => {
|
|
4095
4357
|
const id = sessionId ?? listSessionsForCwd(this.runner.cwd)[0]?.sessionId;
|
|
4096
4358
|
return id ? this.runner.resumeFrom(id) : false;
|
|
4097
4359
|
},
|
|
4098
4360
|
openSessionSwitcher: this.openSessions,
|
|
4099
4361
|
openModelPicker: () => void this.openModels(),
|
|
4362
|
+
hasThinkingLevels: () => this.runner.hasThinkingLevels(),
|
|
4363
|
+
openEffortPicker: () => this.openEffort(),
|
|
4364
|
+
setEffort: (level) => this.setEffort(level),
|
|
4100
4365
|
getModel: () => this.runner.model,
|
|
4101
4366
|
setModel: (model) => this.setModel(model),
|
|
4102
4367
|
getInteractionMode: () => this.runner.interactionMode,
|
|
4368
|
+
getInteractionModes: () => this.runner.modeIds,
|
|
4369
|
+
agentCommands: () => this.agentCommands(),
|
|
4103
4370
|
setInteractionMode: (mode) => this.setInteractionMode(mode),
|
|
4104
4371
|
getPermissionMode: () => this.store.getState().permissionMode,
|
|
4105
4372
|
setPermissionMode: (mode) => this.setPermissionMode(mode),
|
|
4106
4373
|
openPermissionPicker: this.openPermissionModes,
|
|
4107
|
-
|
|
4108
|
-
|
|
4374
|
+
mcpSignIn: async (serverName, signIn) => {
|
|
4375
|
+
try {
|
|
4376
|
+
const status = await this.runner.mcpSignIn(serverName, signIn);
|
|
4377
|
+
this.refresh();
|
|
4378
|
+
const failure = status.failures.find((item) => item.serverName === serverName);
|
|
4379
|
+
if (!signIn) return `Signed out of "${serverName}".`;
|
|
4380
|
+
return failure ? `Signed in, but "${serverName}" did not connect: ${failure.message}` : `Signed in to "${serverName}"; its tools are available from the next message.`;
|
|
4381
|
+
} catch (error) {
|
|
4382
|
+
return errorMessage(error, signIn ? "Sign-in failed." : "Sign-out failed.");
|
|
4383
|
+
}
|
|
4384
|
+
},
|
|
4385
|
+
trustWorkspace: async () => {
|
|
4386
|
+
try {
|
|
4387
|
+
await this.runner.trustWorkspace();
|
|
4388
|
+
} catch (error) {
|
|
4389
|
+
return errorMessage(error, "Could not trust the workspace.");
|
|
4390
|
+
}
|
|
4109
4391
|
return "Workspace trusted: its allow rules apply now; MCP servers it declares start with the next session (/clear).";
|
|
4110
4392
|
},
|
|
4111
4393
|
openToolInspector: this.openToolInspector,
|
|
4112
4394
|
openMcpInspector: this.openMcpInspector,
|
|
4113
4395
|
openSkillInspector: this.openSkillInspector,
|
|
4396
|
+
switchSkill: this.switchSkill,
|
|
4114
4397
|
openUsageInspector: this.openUsageInspector,
|
|
4115
4398
|
steerMessage: (message) => this.runner.steer(message),
|
|
4116
4399
|
queuedMessages: () => this.runner.queuedMessages(),
|
|
@@ -4125,10 +4408,15 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
|
|
|
4125
4408
|
killBackgroundJob: (jobId) => this.runner.killBackgroundJob(jobId),
|
|
4126
4409
|
releaseBackgroundJob: (jobId) => this.runner.releaseBackgroundJob(jobId)
|
|
4127
4410
|
};
|
|
4411
|
+
/** The agent's commands the CLI doesn't have itself; a hint is shown after the description. */
|
|
4412
|
+
agentCommands() {
|
|
4413
|
+
return this.runner.agentCommands.filter((command) => !findCommand(command.name)).map(({ name, description, hint }) => ({ name, description: hint ? `${description} ${hint}` : description }));
|
|
4414
|
+
}
|
|
4128
4415
|
// ------------------------------------------------------------ prompt
|
|
4129
4416
|
submit = async (text, images = [], pastes = []) => {
|
|
4130
4417
|
const value = text.trim();
|
|
4131
4418
|
if (!value) return;
|
|
4419
|
+
this.scroll = 0;
|
|
4132
4420
|
this.editor.addToHistory(value);
|
|
4133
4421
|
this.store.setState((state) => ({
|
|
4134
4422
|
inputHistory: [...state.inputHistory, value],
|
|
@@ -4143,6 +4431,10 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
|
|
|
4143
4431
|
const name = match?.[1] ?? "";
|
|
4144
4432
|
const command = findCommand(name);
|
|
4145
4433
|
if (!command) {
|
|
4434
|
+
if (this.agentCommands().some((agentCommand) => agentCommand.name === name)) {
|
|
4435
|
+
await this.runner.submit(value, images, pastes);
|
|
4436
|
+
return;
|
|
4437
|
+
}
|
|
4146
4438
|
this.appendError(`Unknown command: /${name}`);
|
|
4147
4439
|
return;
|
|
4148
4440
|
}
|
|
@@ -4204,7 +4496,7 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
|
|
|
4204
4496
|
const fdPath = resolveFdPath();
|
|
4205
4497
|
editor.setAutocompleteProvider(
|
|
4206
4498
|
new WorkspaceAutocompleteProvider(
|
|
4207
|
-
allCommands().map((command) => ({
|
|
4499
|
+
() => [...allCommands(this.commandContext), ...this.agentCommands()].map((command) => ({
|
|
4208
4500
|
name: command.name,
|
|
4209
4501
|
description: command.description
|
|
4210
4502
|
})),
|
|
@@ -4230,7 +4522,10 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
|
|
|
4230
4522
|
this.view = this.buildView();
|
|
4231
4523
|
if (this.view.transcript.epoch !== this.lastEpoch) {
|
|
4232
4524
|
this.lastEpoch = this.view.transcript.epoch;
|
|
4233
|
-
this.
|
|
4525
|
+
if (this.scroll) {
|
|
4526
|
+
this.scroll = 0;
|
|
4527
|
+
this.view = this.buildView();
|
|
4528
|
+
}
|
|
4234
4529
|
}
|
|
4235
4530
|
for (const listener of this.listeners) listener();
|
|
4236
4531
|
}
|
|
@@ -4242,9 +4537,11 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
|
|
|
4242
4537
|
overlay: this.overlay,
|
|
4243
4538
|
question: this.questionState(ui.pendingQuestion),
|
|
4244
4539
|
model: this.runner.model,
|
|
4540
|
+
effort: this.effortName(),
|
|
4245
4541
|
mcp: mcpCounts(this.runner),
|
|
4246
4542
|
expandedTools: this.expandedTools,
|
|
4247
|
-
exiting: this.exiting
|
|
4543
|
+
exiting: this.exiting,
|
|
4544
|
+
scroll: this.scroll
|
|
4248
4545
|
};
|
|
4249
4546
|
}
|
|
4250
4547
|
questionState(pending) {
|
|
@@ -4276,18 +4573,16 @@ ${skill.path}`).join("\n\n") : "No skills discovered. Add SKILL.md under .agents
|
|
|
4276
4573
|
}
|
|
4277
4574
|
};
|
|
4278
4575
|
function formatMcpServer(server, status) {
|
|
4279
|
-
const
|
|
4280
|
-
|
|
4281
|
-
if (
|
|
4282
|
-
if ("command" in server) {
|
|
4576
|
+
const lines = [`${server.name} [${server.transport}]`];
|
|
4577
|
+
if (server.url) lines.push(`url: ${server.url}`);
|
|
4578
|
+
if (server.command) {
|
|
4283
4579
|
lines.push(`command: ${server.command}`);
|
|
4284
|
-
if (server.args
|
|
4285
|
-
if (server.env
|
|
4286
|
-
lines.push(`env: ${server.env.map((entry) => entry.name).join(", ")} (values hidden)`);
|
|
4580
|
+
if (server.args?.length) lines.push(`args: ${server.args.join(" ")}`);
|
|
4581
|
+
if (server.env?.length) lines.push(`env: ${server.env.join(", ")} (values hidden)`);
|
|
4287
4582
|
}
|
|
4288
4583
|
const failure = status.failures.find((item) => item.serverName === server.name);
|
|
4289
4584
|
lines.push(
|
|
4290
|
-
failure ? `status: failed - ${failure.message}` : status.connected.includes(server.name) ? "status: connected" : "status: configured; connection starts with the ACP session"
|
|
4585
|
+
failure?.needsAuth ? `status: needs sign-in - /mcp login ${server.name}` : failure ? `status: failed - ${failure.message}` : status.connected.includes(server.name) ? "status: connected" : "status: configured; connection starts with the ACP session"
|
|
4291
4586
|
);
|
|
4292
4587
|
return lines.join("\n");
|
|
4293
4588
|
}
|
|
@@ -4308,12 +4603,13 @@ function errorMessage(error, fallback) {
|
|
|
4308
4603
|
import { Box as Box5, Text as Text5, useInput as useInput2, usePaste as usePaste2 } from "ink";
|
|
4309
4604
|
import stringWidth2 from "string-width";
|
|
4310
4605
|
import { useMemo, useState } from "react";
|
|
4606
|
+
import { OTHER_OPTION_ID as OTHER_OPTION_ID2 } from "@datalabrotterdam/nova-ai-agent/core/user-questions.js";
|
|
4311
4607
|
import { Fragment as Fragment3, jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
4312
4608
|
function useDialogInput(handle, isActive) {
|
|
4313
4609
|
const [, setTick] = useState(0);
|
|
4314
4610
|
useInput2(
|
|
4315
4611
|
(input, key) => {
|
|
4316
|
-
if (handle(input, key) === "changed") setTick((tick) => tick + 1);
|
|
4612
|
+
if (!isMouseInput(input) && handle(input, key) === "changed") setTick((tick) => tick + 1);
|
|
4317
4613
|
},
|
|
4318
4614
|
{ isActive }
|
|
4319
4615
|
);
|
|
@@ -4325,6 +4621,25 @@ function Frame({ title, children, footer }) {
|
|
|
4325
4621
|
footer ? /* @__PURE__ */ jsx5(Text5, { color: palette.faint, wrap: "truncate-end", children: footer }) : null
|
|
4326
4622
|
] });
|
|
4327
4623
|
}
|
|
4624
|
+
function SliderDialog({
|
|
4625
|
+
title,
|
|
4626
|
+
state,
|
|
4627
|
+
isActive = true
|
|
4628
|
+
}) {
|
|
4629
|
+
useDialogInput((input, key) => state.handleKey(input, key), isActive);
|
|
4630
|
+
const current = state.levels[state.selected];
|
|
4631
|
+
return /* @__PURE__ */ jsxs5(Frame, { title, footer: "\u2190\u2192 adjust \xB7 Enter apply \xB7 Esc cancel", children: [
|
|
4632
|
+
/* @__PURE__ */ jsx5(Text5, { wrap: "truncate-end", children: state.levels.map((level, index) => {
|
|
4633
|
+
const selected = index === state.selected;
|
|
4634
|
+
return /* @__PURE__ */ jsxs5(Text5, { children: [
|
|
4635
|
+
index > 0 ? /* @__PURE__ */ jsx5(Text5, { color: palette.faint, children: " \u2500\u2500 " }) : null,
|
|
4636
|
+
/* @__PURE__ */ jsx5(Text5, { color: selected ? palette.primary : palette.faint, children: selected ? "\u25CF " : "\u25CB " }),
|
|
4637
|
+
/* @__PURE__ */ jsx5(Text5, { bold: selected, color: selected ? void 0 : palette.muted, children: level.label })
|
|
4638
|
+
] }, level.value);
|
|
4639
|
+
}) }),
|
|
4640
|
+
current?.description ? /* @__PURE__ */ jsx5(Text5, { color: palette.muted, children: current.description }) : null
|
|
4641
|
+
] });
|
|
4642
|
+
}
|
|
4328
4643
|
function SelectionDialog({
|
|
4329
4644
|
title,
|
|
4330
4645
|
state,
|
|
@@ -4455,7 +4770,7 @@ function QuestionDialog({
|
|
|
4455
4770
|
const [, setTick] = useState(0);
|
|
4456
4771
|
useInput2(
|
|
4457
4772
|
(input, key) => {
|
|
4458
|
-
if (state.handleKey(input, key) === "changed") setTick((tick) => tick + 1);
|
|
4773
|
+
if (!isMouseInput(input) && state.handleKey(input, key) === "changed") setTick((tick) => tick + 1);
|
|
4459
4774
|
},
|
|
4460
4775
|
{ isActive }
|
|
4461
4776
|
);
|
|
@@ -4520,7 +4835,7 @@ function QuestionDialog({
|
|
|
4520
4835
|
/* @__PURE__ */ jsxs5(Text5, { wrap: "truncate-end", children: [
|
|
4521
4836
|
/* @__PURE__ */ jsx5(Text5, { color: palette.primary, children: state.selected === otherIndex ? ">" : " " }),
|
|
4522
4837
|
" ",
|
|
4523
|
-
state.isSelected(question.id,
|
|
4838
|
+
state.isSelected(question.id, OTHER_OPTION_ID2) ? /* @__PURE__ */ jsx5(Text5, { color: palette.success, children: "[x]" }) : "[ ]",
|
|
4524
4839
|
" ",
|
|
4525
4840
|
otherAnswer ? `Other: ${otherAnswer}` : "Other... (write your own answer)"
|
|
4526
4841
|
] }),
|
|
@@ -4537,7 +4852,7 @@ function PermissionDialog({
|
|
|
4537
4852
|
}) {
|
|
4538
4853
|
useInput2(
|
|
4539
4854
|
(input, key) => {
|
|
4540
|
-
if (permissionKey(request, input, key) === "closed") onClose();
|
|
4855
|
+
if (!isMouseInput(input) && permissionKey(request, input, key) === "closed") onClose();
|
|
4541
4856
|
},
|
|
4542
4857
|
{ isActive }
|
|
4543
4858
|
);
|
|
@@ -4588,9 +4903,8 @@ function wrapPlain(text, width) {
|
|
|
4588
4903
|
}
|
|
4589
4904
|
|
|
4590
4905
|
// src/tui/ink/app.tsx
|
|
4591
|
-
import {
|
|
4906
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
4592
4907
|
var FRAME_MS = 120;
|
|
4593
|
-
var RESIZE_SETTLE_MS = 100;
|
|
4594
4908
|
async function runInkTui(credentials, cwd, args, options = {}) {
|
|
4595
4909
|
const store = createStore({
|
|
4596
4910
|
messages: [],
|
|
@@ -4632,22 +4946,15 @@ async function runInkTui(credentials, cwd, args, options = {}) {
|
|
|
4632
4946
|
// Ink otherwise turns non-interactive whenever CI is set in the
|
|
4633
4947
|
// environment; the TUI only runs on a terminal anyway.
|
|
4634
4948
|
interactive: Boolean(stdout.isTTY),
|
|
4949
|
+
// Full screen: the transcript scrolls inside the app (PgUp/PgDn) and the
|
|
4950
|
+
// terminal gets its own screen back on exit.
|
|
4951
|
+
alternateScreen: Boolean(stdout.isTTY),
|
|
4635
4952
|
kittyKeyboard: { mode: kittyKeyboard ? "enabled" : "disabled" }
|
|
4636
4953
|
});
|
|
4637
4954
|
controller.start();
|
|
4955
|
+
if (stdout.isTTY) stdout.write(MOUSE_REPORTING_ON);
|
|
4638
4956
|
const onSignal = () => controller.requestExit();
|
|
4639
4957
|
process.once("SIGTERM", onSignal);
|
|
4640
|
-
let printedColumns = stdout.columns;
|
|
4641
|
-
let resizeTimer;
|
|
4642
|
-
const onResize = () => {
|
|
4643
|
-
if (resizeTimer) clearTimeout(resizeTimer);
|
|
4644
|
-
resizeTimer = setTimeout(() => {
|
|
4645
|
-
if (stdout.columns === printedColumns) return;
|
|
4646
|
-
printedColumns = stdout.columns;
|
|
4647
|
-
controller.redrawAll();
|
|
4648
|
-
}, RESIZE_SETTLE_MS);
|
|
4649
|
-
};
|
|
4650
|
-
stdout.on("resize", onResize);
|
|
4651
4958
|
void (options.checkForUpdate ?? checkForUpdate)().then((updateAvailable) => {
|
|
4652
4959
|
if (updateAvailable) store.setState({ updateAvailable });
|
|
4653
4960
|
});
|
|
@@ -4657,45 +4964,99 @@ async function runInkTui(credentials, cwd, args, options = {}) {
|
|
|
4657
4964
|
await controller.stop();
|
|
4658
4965
|
} finally {
|
|
4659
4966
|
process.off("SIGTERM", onSignal);
|
|
4660
|
-
stdout.
|
|
4661
|
-
if (resizeTimer) clearTimeout(resizeTimer);
|
|
4967
|
+
if (stdout.isTTY) stdout.write(MOUSE_REPORTING_OFF);
|
|
4662
4968
|
instance.unmount();
|
|
4663
4969
|
await instance.waitUntilExit().catch(() => {
|
|
4664
4970
|
});
|
|
4665
4971
|
}
|
|
4666
4972
|
stdout.write(`Resume this session with: nova-ai --resume ${runner.sessionId}
|
|
4667
4973
|
`);
|
|
4974
|
+
return { sessionId: runner.sessionId, updateRequested: controller.updateRequested };
|
|
4668
4975
|
}
|
|
4669
4976
|
function App({ controller }) {
|
|
4670
4977
|
const view = useSyncExternalStore2(controller.subscribe, controller.getView);
|
|
4671
4978
|
const { rows, columns } = useWindowSize();
|
|
4672
4979
|
const bottomRef = useRef(null);
|
|
4673
4980
|
const bottom = useBoxMetrics(bottomRef);
|
|
4981
|
+
const contentRef = useRef(null);
|
|
4982
|
+
const content = useBoxMetrics(contentRef);
|
|
4674
4983
|
const working = useWorking(view);
|
|
4984
|
+
const [userTops, setUserTops] = useState2(/* @__PURE__ */ new Map());
|
|
4985
|
+
useInput3((input) => {
|
|
4986
|
+
const rows2 = wheelRows(input);
|
|
4987
|
+
if (rows2) controller.scrollBy(rows2);
|
|
4988
|
+
});
|
|
4989
|
+
const height = Math.max(4, rows - 1);
|
|
4990
|
+
const viewportRows = Math.max(3, height - (bottom.hasMeasured ? bottom.height : 6));
|
|
4991
|
+
useEffect(() => {
|
|
4992
|
+
if (content.hasMeasured) controller.setViewport(viewportRows, content.height);
|
|
4993
|
+
}, [controller, viewportRows, content.hasMeasured, content.height]);
|
|
4994
|
+
if (view.exiting) return null;
|
|
4675
4995
|
const { transcript } = view;
|
|
4676
|
-
const
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4996
|
+
const blocks = [...transcript.committed, ...transcript.live];
|
|
4997
|
+
const liveKeys = new Set(transcript.live.map((block) => block.key));
|
|
4998
|
+
const onUserTop = (key, top) => setUserTops((tops) => tops.get(key) === top ? tops : new Map(tops).set(key, top));
|
|
4999
|
+
const firstVisibleRow = content.height - viewportRows - view.scroll;
|
|
5000
|
+
let sticky = null;
|
|
5001
|
+
for (const block of blocks) {
|
|
5002
|
+
if (block.kind !== "user") continue;
|
|
5003
|
+
const top = userTops.get(block.key);
|
|
5004
|
+
if (top !== void 0 && top < firstVisibleRow) sticky = block.text;
|
|
5005
|
+
}
|
|
5006
|
+
return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", height, children: [
|
|
5007
|
+
/* @__PURE__ */ jsxs6(
|
|
5008
|
+
Box6,
|
|
5009
|
+
{
|
|
5010
|
+
flexDirection: "column",
|
|
5011
|
+
height: viewportRows,
|
|
5012
|
+
flexShrink: 0,
|
|
5013
|
+
overflowY: "hidden",
|
|
5014
|
+
justifyContent: "flex-end",
|
|
5015
|
+
children: [
|
|
5016
|
+
/* @__PURE__ */ jsxs6(Box6, { ref: contentRef, flexDirection: "column", flexShrink: 0, marginBottom: -view.scroll, children: [
|
|
5017
|
+
blocks.map(
|
|
5018
|
+
(block) => liveKeys.has(block.key) ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block, frame: working.frame }) }, block.key) : /* @__PURE__ */ jsx6(CommittedBlock, { block, onUserTop }, block.key)
|
|
5019
|
+
),
|
|
5020
|
+
working.line ? /* @__PURE__ */ jsx6(Box6, { marginTop: 1, paddingLeft: 1, children: /* @__PURE__ */ jsxs6(Text6, { children: [
|
|
5021
|
+
/* @__PURE__ */ jsx6(Text6, { color: palette.accent, children: WORKING_FRAMES[working.frame] }),
|
|
5022
|
+
/* @__PURE__ */ jsx6(Text6, { color: palette.muted, children: ` ${working.line}` })
|
|
5023
|
+
] }) }) : null
|
|
5024
|
+
] }),
|
|
5025
|
+
sticky !== null ? /* @__PURE__ */ jsx6(StickyPrompt, { text: sticky, columns }) : null
|
|
5026
|
+
]
|
|
5027
|
+
}
|
|
5028
|
+
),
|
|
5029
|
+
/* @__PURE__ */ jsxs6(Box6, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [
|
|
5030
|
+
/* @__PURE__ */ jsx6(Surface, { controller, view, rows: Math.max(6, rows - 4), columns }),
|
|
5031
|
+
/* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, effort: view.effort, mcp: view.mcp, scroll: view.scroll })
|
|
4696
5032
|
] })
|
|
4697
5033
|
] });
|
|
4698
5034
|
}
|
|
5035
|
+
var CommittedBlock = memo(function CommittedBlock2({
|
|
5036
|
+
block,
|
|
5037
|
+
onUserTop
|
|
5038
|
+
}) {
|
|
5039
|
+
if (block.kind !== "user") {
|
|
5040
|
+
return /* @__PURE__ */ jsx6(Box6, { marginTop: 1, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
|
|
5041
|
+
}
|
|
5042
|
+
return /* @__PURE__ */ jsx6(UserAnchor, { block, onUserTop, children: /* @__PURE__ */ jsx6(TranscriptBlockView, { block }) });
|
|
5043
|
+
}, (before, after) => before.block === after.block);
|
|
5044
|
+
function UserAnchor({
|
|
5045
|
+
block,
|
|
5046
|
+
onUserTop,
|
|
5047
|
+
children
|
|
5048
|
+
}) {
|
|
5049
|
+
const ref = useRef(null);
|
|
5050
|
+
const { top, hasMeasured } = useBoxMetrics(ref);
|
|
5051
|
+
useEffect(() => {
|
|
5052
|
+
if (hasMeasured) onUserTop(block.key, top);
|
|
5053
|
+
}, [block.key, top, hasMeasured]);
|
|
5054
|
+
return /* @__PURE__ */ jsx6(Box6, { ref, marginTop: 1, children });
|
|
5055
|
+
}
|
|
5056
|
+
function StickyPrompt({ text, columns }) {
|
|
5057
|
+
const line = `> ${text.replace(/\s+/g, " ").trim()}`;
|
|
5058
|
+
return /* @__PURE__ */ jsx6(Box6, { position: "absolute", top: 0, left: 0, width: columns, children: /* @__PURE__ */ jsx6(Text6, { color: palette.primary, backgroundColor: palette.stickyBackground, wrap: "truncate-end", children: line.padEnd(columns) }) });
|
|
5059
|
+
}
|
|
4699
5060
|
function Surface({
|
|
4700
5061
|
controller,
|
|
4701
5062
|
view,
|
|
@@ -4710,6 +5071,7 @@ function Surface({
|
|
|
4710
5071
|
const overlay = view.overlay;
|
|
4711
5072
|
if (overlay?.kind === "selection")
|
|
4712
5073
|
return /* @__PURE__ */ jsx6(SelectionDialog, { title: overlay.title, state: overlay.state, height: rows });
|
|
5074
|
+
if (overlay?.kind === "slider") return /* @__PURE__ */ jsx6(SliderDialog, { title: overlay.title, state: overlay.state });
|
|
4713
5075
|
if (overlay?.kind === "text")
|
|
4714
5076
|
return /* @__PURE__ */ jsx6(
|
|
4715
5077
|
ScrollPanel,
|
|
@@ -4756,17 +5118,28 @@ function useWorking(view) {
|
|
|
4756
5118
|
}
|
|
4757
5119
|
|
|
4758
5120
|
// src/tui/index.ts
|
|
5121
|
+
import { workspaceNeedsTrust } from "@datalabrotterdam/nova-ai-agent/core/workspace-mcp.js";
|
|
4759
5122
|
async function runChat(options) {
|
|
4760
5123
|
const stored = readCredentials();
|
|
4761
5124
|
if (!stored) {
|
|
4762
5125
|
console.error("nova-ai isn't connected to a Nova AI account yet. Run `nova-ai login` first.");
|
|
4763
5126
|
return 1;
|
|
4764
5127
|
}
|
|
4765
|
-
const
|
|
5128
|
+
const model = options.model ?? stored.defaultModel ?? process.env.NOVA_MODEL;
|
|
5129
|
+
const credentials = { ...stored, defaultModel: model };
|
|
5130
|
+
const updateCheck = checkForUpdate().then((update) => withInstallability(update));
|
|
5131
|
+
const updatedFrom = process.env[UPDATED_FROM_ENV];
|
|
5132
|
+
if (updatedFrom) {
|
|
5133
|
+
delete process.env[UPDATED_FROM_ENV];
|
|
5134
|
+
console.log(`Updated nova-ai ${updatedFrom} \u2192 ${cliVersion() ?? "?"}.`);
|
|
5135
|
+
} else {
|
|
5136
|
+
const restartedCode = await offerUpdateAtStart(updateCheck, process.argv.slice(2));
|
|
5137
|
+
if (restartedCode !== null) return restartedCode;
|
|
5138
|
+
}
|
|
4766
5139
|
const cwd = process.cwd();
|
|
4767
5140
|
let resume = options.resume;
|
|
4768
5141
|
if (options.continueLast) {
|
|
4769
|
-
resume =
|
|
5142
|
+
resume = listStoredSessions2(cwd)[0]?.sessionId ?? null;
|
|
4770
5143
|
if (!resume) {
|
|
4771
5144
|
console.error(`No earlier session in ${cwd} to continue.`);
|
|
4772
5145
|
return 1;
|
|
@@ -4774,11 +5147,22 @@ async function runChat(options) {
|
|
|
4774
5147
|
}
|
|
4775
5148
|
if (workspaceNeedsTrust(cwd)) await askToTrust(cwd);
|
|
4776
5149
|
const args = resume ? ["--resume", resume] : [];
|
|
4777
|
-
await runInkTui(credentials, cwd, args);
|
|
5150
|
+
const result = await runInkTui(credentials, cwd, args, { checkForUpdate: () => updateCheck });
|
|
5151
|
+
if (result.updateRequested) {
|
|
5152
|
+
const update = await updateCheck;
|
|
5153
|
+
if (update) {
|
|
5154
|
+
const restartArgs = [
|
|
5155
|
+
...loadStoredSession2(result.sessionId) ? ["--resume", result.sessionId] : [],
|
|
5156
|
+
...options.model ? ["--model", options.model] : []
|
|
5157
|
+
];
|
|
5158
|
+
const restartedCode = installAndRestart(update, restartArgs);
|
|
5159
|
+
if (restartedCode !== null) return restartedCode;
|
|
5160
|
+
}
|
|
5161
|
+
}
|
|
4778
5162
|
return 0;
|
|
4779
5163
|
}
|
|
4780
5164
|
async function askToTrust(cwd) {
|
|
4781
|
-
const prompt =
|
|
5165
|
+
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
4782
5166
|
try {
|
|
4783
5167
|
console.log(`
|
|
4784
5168
|
${cwd}`);
|