@datalabrotterdam/nova-ai-cli 1.2.0-alpha.5 → 1.2.0-alpha.7
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 +27 -0
- package/README.md +29 -3
- package/dist/{chunk-NHGIUEIR.js → chunk-EOPM4RRE.js} +30 -150
- package/dist/{headless-OFJFVJ3O.js → headless-QLCJRT3X.js} +23 -16
- package/dist/index.js +4 -10
- package/dist/{tui-4XTC6VJP.js → tui-HJQOYGHL.js} +362 -212
- package/package.json +5 -3
- package/dist/webui-L4GGMXJ3.js +0 -9
|
@@ -9,21 +9,14 @@ import {
|
|
|
9
9
|
} from "./chunk-FMHWW3E6.js";
|
|
10
10
|
import {
|
|
11
11
|
TuiAcpClient,
|
|
12
|
-
createStore
|
|
13
|
-
|
|
14
|
-
workspaceNeedsTrust
|
|
15
|
-
} from "./chunk-NHGIUEIR.js";
|
|
12
|
+
createStore
|
|
13
|
+
} from "./chunk-EOPM4RRE.js";
|
|
16
14
|
|
|
17
15
|
// src/tui/index.ts
|
|
18
16
|
import { createInterface as createInterface2 } from "readline/promises";
|
|
19
|
-
import {
|
|
20
|
-
readCredentials,
|
|
21
|
-
saveDefaultModel as saveDefaultModel2
|
|
22
|
-
} from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
|
|
23
|
-
import { resolveDefaultModel as resolveDefaultModel2 } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
|
|
24
|
-
import { createNovaClient as createNovaClient2 } from "@datalabrotterdam/nova-ai-agent/core/nova-client.js";
|
|
17
|
+
import { readCredentials } from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
|
|
25
18
|
import { listStoredSessions as listStoredSessions2, loadStoredSession as loadStoredSession2 } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
|
|
26
|
-
import { setWorkspaceTrusted
|
|
19
|
+
import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
|
|
27
20
|
|
|
28
21
|
// src/update-install.ts
|
|
29
22
|
import { spawnSync } from "child_process";
|
|
@@ -126,27 +119,18 @@ import { savedPermissionMode } from "@datalabrotterdam/nova-ai-agent/core/policy
|
|
|
126
119
|
|
|
127
120
|
// src/tui/session/session-runner.ts
|
|
128
121
|
import * as acp from "@agentclientprotocol/sdk";
|
|
129
|
-
import {
|
|
130
|
-
NovaAIError
|
|
131
|
-
} from "@datalabrotterdam/nova-sdk";
|
|
132
|
-
import { createNovaClient } from "@datalabrotterdam/nova-ai-agent/core/nova-client.js";
|
|
133
122
|
import { NovaAgent } from "@datalabrotterdam/nova-ai-agent/acp/agent.js";
|
|
134
123
|
import { startInProcessAgent } from "@datalabrotterdam/nova-ai-agent/client/in-process.js";
|
|
135
124
|
import { replayHistory } from "@datalabrotterdam/nova-ai-agent/core/history.js";
|
|
136
|
-
import {
|
|
125
|
+
import {
|
|
126
|
+
saveDefaultEffort,
|
|
127
|
+
saveDefaultModel
|
|
128
|
+
} from "@datalabrotterdam/nova-ai-agent/core/credentials.js";
|
|
137
129
|
import {
|
|
138
130
|
savePermissionMode
|
|
139
131
|
} from "@datalabrotterdam/nova-ai-agent/core/policy/settings.js";
|
|
140
132
|
import { loadStoredSession } from "@datalabrotterdam/nova-ai-agent/core/sessions.js";
|
|
141
|
-
import {
|
|
142
|
-
import {
|
|
143
|
-
resolveModelContextWindow
|
|
144
|
-
} from "@datalabrotterdam/nova-ai-agent/core/context-compaction.js";
|
|
145
|
-
import {
|
|
146
|
-
chatModels,
|
|
147
|
-
resolveDefaultModel,
|
|
148
|
-
resolveModelSupportsImageInput
|
|
149
|
-
} from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
|
|
133
|
+
import { resolveEffort } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
|
|
150
134
|
|
|
151
135
|
// src/tui/files/file-mentions.ts
|
|
152
136
|
import { statSync } from "fs";
|
|
@@ -291,15 +275,6 @@ var SessionRunner = class {
|
|
|
291
275
|
constructor(store, credentials, cwd) {
|
|
292
276
|
this.store = store;
|
|
293
277
|
this.credentials = credentials;
|
|
294
|
-
this.loadMcpConfiguration(cwd);
|
|
295
|
-
this.skills = discoverSkills(cwd).map(
|
|
296
|
-
({ name, description, source, path }) => ({
|
|
297
|
-
name,
|
|
298
|
-
description,
|
|
299
|
-
source,
|
|
300
|
-
path
|
|
301
|
-
})
|
|
302
|
-
);
|
|
303
278
|
this.cwd = cwd;
|
|
304
279
|
this.acpClient = new TuiAcpClient(
|
|
305
280
|
store,
|
|
@@ -314,13 +289,8 @@ var SessionRunner = class {
|
|
|
314
289
|
clientCapabilities: this.acpClient.capabilities,
|
|
315
290
|
clientInfo: { name: "nova-ai-cli-tui", version: "1" }
|
|
316
291
|
});
|
|
317
|
-
this.
|
|
318
|
-
|
|
319
|
-
if (!model)
|
|
320
|
-
throw new Error(
|
|
321
|
-
"No Nova model configured. Re-run authentication or set NOVA_MODEL."
|
|
322
|
-
);
|
|
323
|
-
this.model = model;
|
|
292
|
+
this.model = credentials.defaultModel ?? process.env.NOVA_MODEL ?? "";
|
|
293
|
+
this.effort = credentials.defaultEffort ?? null;
|
|
324
294
|
this.sessionId = crypto.randomUUID();
|
|
325
295
|
this.sessionReady = Promise.resolve();
|
|
326
296
|
}
|
|
@@ -331,21 +301,22 @@ var SessionRunner = class {
|
|
|
331
301
|
title = null;
|
|
332
302
|
model;
|
|
333
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
|
+
}
|
|
334
308
|
/** The agent, served in-process over a real ACP connection (tests may stub it). */
|
|
335
309
|
agent = new NovaAgent();
|
|
336
310
|
acpClient;
|
|
337
311
|
connection;
|
|
338
312
|
nova;
|
|
339
313
|
initialized;
|
|
340
|
-
novaClient;
|
|
341
|
-
mcpServers = [];
|
|
342
|
-
mcpConfigurationFailures = [];
|
|
343
314
|
mcpStatus = {
|
|
344
315
|
configured: [],
|
|
345
316
|
connected: [],
|
|
346
317
|
failures: []
|
|
347
318
|
};
|
|
348
|
-
skills;
|
|
319
|
+
skills = [];
|
|
349
320
|
sessionReady;
|
|
350
321
|
agentSessionLoaded = false;
|
|
351
322
|
promptActive = false;
|
|
@@ -353,10 +324,10 @@ var SessionRunner = class {
|
|
|
353
324
|
handoffActive = false;
|
|
354
325
|
queuedAttachments = /* @__PURE__ */ new Map();
|
|
355
326
|
editingQueuedMessageId = null;
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
327
|
+
/** From the agent's model list: image input, thinking levels. */
|
|
328
|
+
modelInfo = /* @__PURE__ */ new Map();
|
|
329
|
+
/** The thinking level the user chose (saved); null: each model's default. */
|
|
330
|
+
effort;
|
|
360
331
|
contextUsageRequest = 0;
|
|
361
332
|
modelCheck = null;
|
|
362
333
|
resumeFrom(storedSessionId) {
|
|
@@ -369,15 +340,7 @@ var SessionRunner = class {
|
|
|
369
340
|
this.sessionId = stored.sessionId;
|
|
370
341
|
this.cwd = stored.cwd;
|
|
371
342
|
this.title = stored.title;
|
|
372
|
-
this.
|
|
373
|
-
this.skills = discoverSkills(stored.cwd).map(
|
|
374
|
-
({ name, description, source, path }) => ({
|
|
375
|
-
name,
|
|
376
|
-
description,
|
|
377
|
-
source,
|
|
378
|
-
path
|
|
379
|
-
})
|
|
380
|
-
);
|
|
343
|
+
this.skills = [];
|
|
381
344
|
const messages = restoreSessionMessages(stored.messages);
|
|
382
345
|
this.store.setState({
|
|
383
346
|
messages,
|
|
@@ -396,12 +359,13 @@ var SessionRunner = class {
|
|
|
396
359
|
return await this.nova.loadSession({
|
|
397
360
|
sessionId: stored.sessionId,
|
|
398
361
|
cwd: stored.cwd,
|
|
399
|
-
mcpServers:
|
|
362
|
+
mcpServers: []
|
|
400
363
|
});
|
|
401
364
|
} finally {
|
|
402
365
|
this.acpClient.muteSessionUpdates = false;
|
|
403
366
|
}
|
|
404
367
|
}).then((response) => {
|
|
368
|
+
this.applyModes(response.modes);
|
|
405
369
|
this.applyMcpStatus(response._meta);
|
|
406
370
|
this.applySkillStatus(response._meta);
|
|
407
371
|
this.agentSessionLoaded = true;
|
|
@@ -409,6 +373,7 @@ var SessionRunner = class {
|
|
|
409
373
|
void this.nova.setSessionMode({ sessionId: this.sessionId, modeId: this.interactionMode }).catch(() => {
|
|
410
374
|
});
|
|
411
375
|
this.applyPermissionMode(this.sessionId);
|
|
376
|
+
this.applyEffort(this.sessionId);
|
|
412
377
|
void this.updateContextUsage();
|
|
413
378
|
void this.refreshContextUsage().catch(() => {
|
|
414
379
|
});
|
|
@@ -521,9 +486,6 @@ var SessionRunner = class {
|
|
|
521
486
|
void this.enqueueQueuedPrompt(value, [], [], "steer", true);
|
|
522
487
|
return true;
|
|
523
488
|
}
|
|
524
|
-
configuredMcpServers() {
|
|
525
|
-
return [...this.mcpServers];
|
|
526
|
-
}
|
|
527
489
|
mcpSessionStatus() {
|
|
528
490
|
return {
|
|
529
491
|
configured: this.mcpStatus.configured.map((server) => ({ ...server })),
|
|
@@ -534,27 +496,37 @@ var SessionRunner = class {
|
|
|
534
496
|
skillSessionStatus() {
|
|
535
497
|
return this.skills.map((skill) => ({ ...skill }));
|
|
536
498
|
}
|
|
499
|
+
/** Signs in to (or out of) an HTTP MCP server; the agent reconnects it and reports the new status. */
|
|
500
|
+
async mcpSignIn(serverName, signIn) {
|
|
501
|
+
await this.ensureSession();
|
|
502
|
+
const params = { sessionId: this.sessionId, serverName };
|
|
503
|
+
const meta = signIn ? await this.nova.mcpLogin(params) : await this.nova.mcpLogout(params);
|
|
504
|
+
this.applyMcpStatus(meta);
|
|
505
|
+
return this.mcpSessionStatus();
|
|
506
|
+
}
|
|
507
|
+
async trustWorkspace() {
|
|
508
|
+
await this.initialized;
|
|
509
|
+
await this.nova.workspaceTrust({ cwd: this.cwd, trusted: true });
|
|
510
|
+
}
|
|
511
|
+
/** Every skill of the workspace with its on/off state, from the agent. */
|
|
512
|
+
async listSkills() {
|
|
513
|
+
await this.initialized;
|
|
514
|
+
return (await this.nova.skillsList({ cwd: this.cwd })).skills;
|
|
515
|
+
}
|
|
516
|
+
async setSkillEnabled(name, enabled, global) {
|
|
517
|
+
await this.initialized;
|
|
518
|
+
return (await this.nova.skillsSetEnabled({
|
|
519
|
+
cwd: this.cwd,
|
|
520
|
+
name,
|
|
521
|
+
enabled,
|
|
522
|
+
scope: global ? "global" : "project"
|
|
523
|
+
})).skills;
|
|
524
|
+
}
|
|
537
525
|
contextUsageSnapshot() {
|
|
538
526
|
return this.store.getState().contextUsage;
|
|
539
527
|
}
|
|
540
528
|
async refreshContextUsage() {
|
|
541
529
|
await this.ensureSession();
|
|
542
|
-
const sessionId = this.sessionId;
|
|
543
|
-
const model = this.model;
|
|
544
|
-
if (this.contextWindowModel !== model) {
|
|
545
|
-
const request = ++this.contextWindowRequest;
|
|
546
|
-
const contextWindow = await resolveModelContextWindow(
|
|
547
|
-
this.novaClient,
|
|
548
|
-
model
|
|
549
|
-
);
|
|
550
|
-
if (request === this.contextWindowRequest && model === this.model) {
|
|
551
|
-
this.contextWindow = contextWindow;
|
|
552
|
-
this.contextWindowModel = model;
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
if (sessionId !== this.sessionId || !this.agentSessionLoaded) {
|
|
556
|
-
return this.refreshContextUsage();
|
|
557
|
-
}
|
|
558
530
|
const usage = await this.updateContextUsage();
|
|
559
531
|
if (!usage)
|
|
560
532
|
throw new Error(
|
|
@@ -570,13 +542,8 @@ var SessionRunner = class {
|
|
|
570
542
|
}
|
|
571
543
|
setModel(model) {
|
|
572
544
|
this.model = model;
|
|
573
|
-
this.contextWindow = null;
|
|
574
|
-
this.contextWindowModel = null;
|
|
575
|
-
this.contextWindowRequest++;
|
|
576
545
|
saveDefaultModel(model);
|
|
577
546
|
if (this.agentSessionLoaded) void this.updateContextUsage();
|
|
578
|
-
void this.refreshContextUsage().catch(() => {
|
|
579
|
-
});
|
|
580
547
|
}
|
|
581
548
|
/**
|
|
582
549
|
* Hands the mode to the agent's permission policy (which decides) and
|
|
@@ -634,27 +601,34 @@ var SessionRunner = class {
|
|
|
634
601
|
}
|
|
635
602
|
/** Chat models only: embedding, speech and transcription models can't run a session. */
|
|
636
603
|
async listModels() {
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
604
|
+
await this.initialized;
|
|
605
|
+
const { models } = await this.nova.modelsList();
|
|
606
|
+
for (const model of models) this.modelInfo.set(model.id, model);
|
|
607
|
+
return models.map((model) => ({ id: model.id, name: model.name, thinking: model.thinking }));
|
|
608
|
+
}
|
|
609
|
+
/** Sync, from the loaded model list: false until it has loaded. */
|
|
610
|
+
hasThinkingLevels(model = this.model) {
|
|
611
|
+
return (this.modelInfo.get(model)?.efforts.length ?? 0) > 0;
|
|
642
612
|
}
|
|
643
613
|
/**
|
|
644
|
-
*
|
|
645
|
-
*
|
|
614
|
+
* Takes the account's default model when none is set, and replaces a saved
|
|
615
|
+
* one that cannot chat (older logins saved the first model of the list,
|
|
616
|
+
* e.g. an embedding model); the agent decides, the choice is saved.
|
|
646
617
|
* Runs once; a failed lookup keeps the model and the prompt reports errors.
|
|
647
618
|
*/
|
|
648
619
|
ensureUsableModel() {
|
|
649
620
|
this.modelCheck ??= (async () => {
|
|
650
621
|
try {
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
|
|
622
|
+
await this.initialized;
|
|
623
|
+
const { models, defaultModel } = await this.nova.modelsList(
|
|
624
|
+
this.model ? { preferred: this.model } : {}
|
|
625
|
+
);
|
|
626
|
+
for (const model of models) this.modelInfo.set(model.id, model);
|
|
627
|
+
if (!defaultModel || defaultModel === this.model) return;
|
|
654
628
|
const previous = this.model;
|
|
655
|
-
this.setModel(
|
|
629
|
+
this.setModel(defaultModel);
|
|
656
630
|
this.store.setState({
|
|
657
|
-
statusLine: `${previous} is not a chat model; switched to ${
|
|
631
|
+
statusLine: previous ? `${previous} is not a chat model; switched to ${defaultModel} (Ctrl+P to pick another).` : `Using model ${defaultModel} (Ctrl+P switches).`
|
|
658
632
|
});
|
|
659
633
|
} catch {
|
|
660
634
|
}
|
|
@@ -662,14 +636,41 @@ var SessionRunner = class {
|
|
|
662
636
|
return this.modelCheck;
|
|
663
637
|
}
|
|
664
638
|
async supportsImageInput(model = this.model) {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
);
|
|
671
|
-
|
|
672
|
-
|
|
639
|
+
return (await this.modelDetails(model))?.imageInput ?? false;
|
|
640
|
+
}
|
|
641
|
+
/** What the agent's model list says about `model`; undefined when it isn't listed. */
|
|
642
|
+
async modelDetails(model = this.model) {
|
|
643
|
+
if (!this.modelInfo.has(model)) await this.listModels();
|
|
644
|
+
return this.modelInfo.get(model);
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* The level the current model thinks at, as the agent picks it (the
|
|
648
|
+
* chosen one when the model has it); null for a model without levels
|
|
649
|
+
* or before the model list has loaded.
|
|
650
|
+
*/
|
|
651
|
+
currentEffort() {
|
|
652
|
+
const info = this.modelInfo.get(this.model);
|
|
653
|
+
if (!info?.efforts.length) return null;
|
|
654
|
+
return resolveEffort(
|
|
655
|
+
{ reasoning_efforts: info.efforts, reasoning_default: info.reasoningDefault },
|
|
656
|
+
this.effort
|
|
657
|
+
) ?? null;
|
|
658
|
+
}
|
|
659
|
+
/** The current model's levels from the loaded list; empty when it has none. */
|
|
660
|
+
modelEffortLevels() {
|
|
661
|
+
return this.modelInfo.get(this.model)?.efforts ?? [];
|
|
662
|
+
}
|
|
663
|
+
/** Saved and kept across models and sessions; a model without the level uses its default. */
|
|
664
|
+
setEffort(level) {
|
|
665
|
+
this.effort = level;
|
|
666
|
+
saveDefaultEffort(level);
|
|
667
|
+
if (this.agentSessionLoaded) this.applyEffort(this.sessionId);
|
|
668
|
+
}
|
|
669
|
+
applyEffort(sessionId) {
|
|
670
|
+
if (!this.effort) return;
|
|
671
|
+
void this.nova.setSessionConfigOption({ sessionId, configId: "thought_level", value: this.effort }).catch(() => {
|
|
672
|
+
this.acpClient.appendError("Could not change the thinking level.");
|
|
673
|
+
});
|
|
673
674
|
}
|
|
674
675
|
async compactContext() {
|
|
675
676
|
if (this.store.getState().busy || this.promptActive) {
|
|
@@ -793,7 +794,7 @@ var SessionRunner = class {
|
|
|
793
794
|
});
|
|
794
795
|
}
|
|
795
796
|
} catch (err) {
|
|
796
|
-
const message = err instanceof
|
|
797
|
+
const message = err instanceof Error ? err.message : "Unknown error.";
|
|
797
798
|
pendingToolFailure = `Request failed: ${message}`;
|
|
798
799
|
this.acpClient.appendError(message);
|
|
799
800
|
this.store.setState({ statusLine: "Request failed." });
|
|
@@ -947,13 +948,13 @@ var SessionRunner = class {
|
|
|
947
948
|
];
|
|
948
949
|
}
|
|
949
950
|
async createSession() {
|
|
950
|
-
this.loadMcpConfiguration(this.cwd);
|
|
951
951
|
await this.initialized;
|
|
952
|
-
const { sessionId, _meta } = await this.nova.newSession({
|
|
952
|
+
const { sessionId, _meta, modes } = await this.nova.newSession({
|
|
953
953
|
cwd: this.cwd,
|
|
954
|
-
mcpServers:
|
|
954
|
+
mcpServers: []
|
|
955
955
|
});
|
|
956
956
|
this.sessionId = sessionId;
|
|
957
|
+
this.applyModes(modes);
|
|
957
958
|
this.applyMcpStatus(_meta);
|
|
958
959
|
this.applySkillStatus(_meta);
|
|
959
960
|
this.agentSessionLoaded = true;
|
|
@@ -961,6 +962,7 @@ var SessionRunner = class {
|
|
|
961
962
|
void this.nova.setSessionMode({ sessionId, modeId: this.interactionMode }).catch(() => {
|
|
962
963
|
});
|
|
963
964
|
this.applyPermissionMode(sessionId);
|
|
965
|
+
this.applyEffort(sessionId);
|
|
964
966
|
void this.updateContextUsage();
|
|
965
967
|
void this.refreshContextUsage().catch(() => {
|
|
966
968
|
});
|
|
@@ -975,10 +977,20 @@ var SessionRunner = class {
|
|
|
975
977
|
const value = meta?.["nova-ai-cli/mcp"];
|
|
976
978
|
if (!value || typeof value !== "object") return;
|
|
977
979
|
const record = value;
|
|
980
|
+
const strings = (value2) => Array.isArray(value2) ? value2.filter((entry) => typeof entry === "string") : void 0;
|
|
978
981
|
const configured = Array.isArray(record.configured) ? record.configured.flatMap((entry) => {
|
|
979
982
|
if (!entry || typeof entry !== "object") return [];
|
|
980
983
|
const item = entry;
|
|
981
|
-
return typeof item.name === "string" && typeof item.transport === "string" ? [
|
|
984
|
+
return typeof item.name === "string" && typeof item.transport === "string" ? [
|
|
985
|
+
{
|
|
986
|
+
name: item.name,
|
|
987
|
+
transport: item.transport,
|
|
988
|
+
...typeof item.url === "string" ? { url: item.url } : {},
|
|
989
|
+
...typeof item.command === "string" ? { command: item.command } : {},
|
|
990
|
+
...strings(item.args) ? { args: strings(item.args) } : {},
|
|
991
|
+
...strings(item.env) ? { env: strings(item.env) } : {}
|
|
992
|
+
}
|
|
993
|
+
] : [];
|
|
982
994
|
}) : [];
|
|
983
995
|
const connected = Array.isArray(record.connected) ? record.connected.filter(
|
|
984
996
|
(entry) => typeof entry === "string"
|
|
@@ -986,26 +998,15 @@ var SessionRunner = class {
|
|
|
986
998
|
const failures = Array.isArray(record.failures) ? record.failures.flatMap((entry) => {
|
|
987
999
|
if (!entry || typeof entry !== "object") return [];
|
|
988
1000
|
const item = entry;
|
|
989
|
-
return typeof item.serverName === "string" && typeof item.message === "string" ? [
|
|
1001
|
+
return typeof item.serverName === "string" && typeof item.message === "string" ? [
|
|
1002
|
+
{
|
|
1003
|
+
serverName: item.serverName,
|
|
1004
|
+
message: item.message,
|
|
1005
|
+
...item.needsAuth === true ? { needsAuth: true } : {}
|
|
1006
|
+
}
|
|
1007
|
+
] : [];
|
|
990
1008
|
}) : [];
|
|
991
|
-
this.mcpStatus = {
|
|
992
|
-
configured,
|
|
993
|
-
connected,
|
|
994
|
-
failures: [...this.mcpConfigurationFailures, ...failures]
|
|
995
|
-
};
|
|
996
|
-
}
|
|
997
|
-
loadMcpConfiguration(cwd) {
|
|
998
|
-
const configuration = readWorkspaceMcpConfiguration(cwd);
|
|
999
|
-
this.mcpServers = configuration.servers;
|
|
1000
|
-
this.mcpConfigurationFailures = configuration.failures;
|
|
1001
|
-
this.mcpStatus = {
|
|
1002
|
-
configured: this.mcpServers.map((server) => ({
|
|
1003
|
-
name: server.name,
|
|
1004
|
-
transport: "type" in server ? server.type : "stdio"
|
|
1005
|
-
})),
|
|
1006
|
-
connected: [],
|
|
1007
|
-
failures: [...this.mcpConfigurationFailures]
|
|
1008
|
-
};
|
|
1009
|
+
this.mcpStatus = { configured, connected, failures };
|
|
1009
1010
|
}
|
|
1010
1011
|
applySkillStatus(meta) {
|
|
1011
1012
|
const value = meta?.["nova-ai-cli/skills"];
|
|
@@ -1023,14 +1024,34 @@ var SessionRunner = class {
|
|
|
1023
1024
|
] : [];
|
|
1024
1025
|
});
|
|
1025
1026
|
}
|
|
1027
|
+
/**
|
|
1028
|
+
* Starts the session ahead of the first prompt, so the agent's MCP and
|
|
1029
|
+
* skill status is there for /mcp and the status line. A failure is left to
|
|
1030
|
+
* the first prompt, which tries again and reports it.
|
|
1031
|
+
*/
|
|
1032
|
+
async prepareSession() {
|
|
1033
|
+
try {
|
|
1034
|
+
await this.ensureSession();
|
|
1035
|
+
} catch {
|
|
1036
|
+
this.sessionReady = Promise.resolve();
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
get sessionLoaded() {
|
|
1040
|
+
return this.agentSessionLoaded;
|
|
1041
|
+
}
|
|
1026
1042
|
async ensureSession() {
|
|
1027
1043
|
await this.ensureUsableModel();
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1044
|
+
for (; ; ) {
|
|
1045
|
+
const ready = this.sessionReady;
|
|
1046
|
+
await ready;
|
|
1047
|
+
if (this.agentSessionLoaded) return;
|
|
1048
|
+
if (this.sessionReady === ready) this.sessionReady = this.createSession();
|
|
1032
1049
|
}
|
|
1033
1050
|
}
|
|
1051
|
+
applyModes(modes) {
|
|
1052
|
+
if (modes?.availableModes.length) this.acpClient.modeIds = modes.availableModes.map((mode) => mode.id);
|
|
1053
|
+
if (!this.modeIds.includes(this.interactionMode)) this.applyInteractionMode("agent", "Mode: agent");
|
|
1054
|
+
}
|
|
1034
1055
|
applyInteractionMode(mode, statusLine) {
|
|
1035
1056
|
this.interactionMode = mode;
|
|
1036
1057
|
this.store.setState({ interactionMode: mode, statusLine });
|
|
@@ -1044,7 +1065,7 @@ var SessionRunner = class {
|
|
|
1044
1065
|
try {
|
|
1045
1066
|
const usage = await this.nova.sessionContextUsage({
|
|
1046
1067
|
sessionId,
|
|
1047
|
-
|
|
1068
|
+
...this.model ? { model: this.model } : {},
|
|
1048
1069
|
mode: this.interactionMode
|
|
1049
1070
|
});
|
|
1050
1071
|
if (request === this.contextUsageRequest && sessionId === this.sessionId) {
|
|
@@ -1500,6 +1521,7 @@ import { Fragment as Fragment2, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-run
|
|
|
1500
1521
|
function StatusLine({
|
|
1501
1522
|
ui,
|
|
1502
1523
|
model,
|
|
1524
|
+
effort = null,
|
|
1503
1525
|
mcp,
|
|
1504
1526
|
scroll = 0
|
|
1505
1527
|
}) {
|
|
@@ -1510,6 +1532,7 @@ function StatusLine({
|
|
|
1510
1532
|
ui.statusLine ? /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: ui.statusLine }) : null,
|
|
1511
1533
|
/* @__PURE__ */ jsxs2(Text2, { wrap: "truncate-end", children: [
|
|
1512
1534
|
/* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: model }),
|
|
1535
|
+
effort ? /* @__PURE__ */ jsx2(Text2, { color: palette.muted, children: ` \xB7 thinking ${effort.toLowerCase()}` }) : null,
|
|
1513
1536
|
separator,
|
|
1514
1537
|
ui.interactionMode,
|
|
1515
1538
|
separator,
|
|
@@ -1852,24 +1875,14 @@ function PlanView({
|
|
|
1852
1875
|
] });
|
|
1853
1876
|
}
|
|
1854
1877
|
|
|
1855
|
-
// src/tui/ink/controller.ts
|
|
1856
|
-
import { setWorkspaceTrusted } from "@datalabrotterdam/nova-ai-agent/core/nova-home.js";
|
|
1857
|
-
import {
|
|
1858
|
-
disabledSkillNames,
|
|
1859
|
-
discoverSkills as discoverSkills2,
|
|
1860
|
-
setSkillEnabled
|
|
1861
|
-
} from "@datalabrotterdam/nova-ai-agent/core/skills.js";
|
|
1862
|
-
import { INTERACTION_MODES } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
|
|
1863
|
-
|
|
1864
1878
|
// src/tui/commands/builtins.ts
|
|
1865
|
-
import { isInteractionMode } from "@datalabrotterdam/nova-ai-agent/core/interaction-modes.js";
|
|
1866
1879
|
var permissionModes = ["ask", "acceptEdits", "bypassAll"];
|
|
1867
1880
|
var builtinCommands = [
|
|
1868
1881
|
{
|
|
1869
1882
|
name: "help",
|
|
1870
1883
|
description: "Show available commands",
|
|
1871
1884
|
run(ctx) {
|
|
1872
|
-
const lines = builtinCommands.map((c) => `/${c.name} \u2014 ${c.description}`);
|
|
1885
|
+
const lines = builtinCommands.filter((c) => c.available?.(ctx) ?? true).map((c) => `/${c.name} \u2014 ${c.description}`);
|
|
1873
1886
|
ctx.print(lines.join("\n"));
|
|
1874
1887
|
}
|
|
1875
1888
|
},
|
|
@@ -1950,15 +1963,15 @@ var builtinCommands = [
|
|
|
1950
1963
|
},
|
|
1951
1964
|
{
|
|
1952
1965
|
name: "mode",
|
|
1953
|
-
description: "Switch interaction mode (/mode agent|ask|plan)",
|
|
1966
|
+
description: "Switch interaction mode (/mode agent|ask|plan|<agent file>)",
|
|
1954
1967
|
run(ctx, args) {
|
|
1955
1968
|
const mode = args.trim();
|
|
1956
1969
|
if (!mode) {
|
|
1957
|
-
ctx.print(`Current mode: ${ctx.getInteractionMode()}`);
|
|
1970
|
+
ctx.print(`Current mode: ${ctx.getInteractionMode()} (available: ${ctx.getInteractionModes().join(", ")})`);
|
|
1958
1971
|
return;
|
|
1959
1972
|
}
|
|
1960
|
-
if (!
|
|
1961
|
-
ctx.print(
|
|
1973
|
+
if (!ctx.getInteractionModes().includes(mode)) {
|
|
1974
|
+
ctx.print(`Usage: /mode ${ctx.getInteractionModes().join("|")}`);
|
|
1962
1975
|
return;
|
|
1963
1976
|
}
|
|
1964
1977
|
ctx.setInteractionMode(mode);
|
|
@@ -1983,8 +1996,8 @@ var builtinCommands = [
|
|
|
1983
1996
|
{
|
|
1984
1997
|
name: "trust",
|
|
1985
1998
|
description: "Trust this workspace: start the MCP servers it declares and apply its .nova-ai allow rules",
|
|
1986
|
-
run(ctx) {
|
|
1987
|
-
ctx.print(ctx.trustWorkspace());
|
|
1999
|
+
async run(ctx) {
|
|
2000
|
+
ctx.print(await ctx.trustWorkspace());
|
|
1988
2001
|
}
|
|
1989
2002
|
},
|
|
1990
2003
|
{
|
|
@@ -2000,6 +2013,16 @@ var builtinCommands = [
|
|
|
2000
2013
|
ctx.openModelPicker();
|
|
2001
2014
|
}
|
|
2002
2015
|
},
|
|
2016
|
+
{
|
|
2017
|
+
name: "effort",
|
|
2018
|
+
description: "Set how hard the model thinks (/effort <level>, or no args for a slider)",
|
|
2019
|
+
available: (ctx) => ctx.hasThinkingLevels(),
|
|
2020
|
+
async run(ctx, args) {
|
|
2021
|
+
const level = args.trim();
|
|
2022
|
+
if (level) ctx.print(await ctx.setEffort(level));
|
|
2023
|
+
else await ctx.openEffortPicker();
|
|
2024
|
+
}
|
|
2025
|
+
},
|
|
2003
2026
|
{
|
|
2004
2027
|
name: "tools",
|
|
2005
2028
|
description: "Inspect tool calls from this session",
|
|
@@ -2009,18 +2032,29 @@ var builtinCommands = [
|
|
|
2009
2032
|
},
|
|
2010
2033
|
{
|
|
2011
2034
|
name: "mcp",
|
|
2012
|
-
description: "Inspect
|
|
2013
|
-
run(ctx) {
|
|
2014
|
-
|
|
2035
|
+
description: "Inspect MCP servers, or sign in to one: /mcp login|logout <server>",
|
|
2036
|
+
async run(ctx, args) {
|
|
2037
|
+
const [action, ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
2038
|
+
const name = rest.join(" ");
|
|
2039
|
+
if (!action) {
|
|
2040
|
+
await ctx.openMcpInspector();
|
|
2041
|
+
return;
|
|
2042
|
+
}
|
|
2043
|
+
if (action !== "login" && action !== "logout" || !name) {
|
|
2044
|
+
ctx.print("Usage: /mcp, or /mcp login|logout <server>");
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
if (action === "login") ctx.print(`Signing in to "${name}"\u2026 (waits up to 5 minutes for the browser)`);
|
|
2048
|
+
ctx.print(await ctx.mcpSignIn(name, action === "login"));
|
|
2015
2049
|
}
|
|
2016
2050
|
},
|
|
2017
2051
|
{
|
|
2018
2052
|
name: "skills",
|
|
2019
2053
|
description: "List skills, or switch one: /skills on|off <name> [--global]",
|
|
2020
|
-
run(ctx, args) {
|
|
2054
|
+
async run(ctx, args) {
|
|
2021
2055
|
const words = args.trim().split(/\s+/).filter(Boolean);
|
|
2022
2056
|
if (!words.length) {
|
|
2023
|
-
ctx.openSkillInspector();
|
|
2057
|
+
await ctx.openSkillInspector();
|
|
2024
2058
|
return;
|
|
2025
2059
|
}
|
|
2026
2060
|
const global = words.includes("--global");
|
|
@@ -2029,7 +2063,7 @@ var builtinCommands = [
|
|
|
2029
2063
|
ctx.print("Usage: /skills, or /skills on|off <name> [--global]");
|
|
2030
2064
|
return;
|
|
2031
2065
|
}
|
|
2032
|
-
ctx.print(ctx.switchSkill(name, action === "on", global));
|
|
2066
|
+
ctx.print(await ctx.switchSkill(name, action === "on", global));
|
|
2033
2067
|
}
|
|
2034
2068
|
},
|
|
2035
2069
|
{
|
|
@@ -2198,8 +2232,8 @@ function formatJob(job) {
|
|
|
2198
2232
|
|
|
2199
2233
|
// src/tui/commands/index.ts
|
|
2200
2234
|
var COMMANDS = [...builtinCommands];
|
|
2201
|
-
function allCommands() {
|
|
2202
|
-
return COMMANDS;
|
|
2235
|
+
function allCommands(ctx) {
|
|
2236
|
+
return ctx ? COMMANDS.filter((c) => c.available?.(ctx) ?? true) : COMMANDS;
|
|
2203
2237
|
}
|
|
2204
2238
|
function findCommand(name) {
|
|
2205
2239
|
return COMMANDS.find((c) => c.name === name);
|
|
@@ -2417,7 +2451,8 @@ var WorkspaceAutocompleteProvider = class {
|
|
|
2417
2451
|
const mention = extractMentionPrefix(before);
|
|
2418
2452
|
if (mention) return this.mentionSuggestions(mention, options.signal);
|
|
2419
2453
|
if (cursorLine === 0 && before.startsWith("/") && !/\s/.test(before)) {
|
|
2420
|
-
const
|
|
2454
|
+
const commands = typeof this.commands === "function" ? this.commands() : this.commands;
|
|
2455
|
+
const items2 = filterCommands(commands, before.slice(1));
|
|
2421
2456
|
return items2.length ? { items: items2, prefix: before } : null;
|
|
2422
2457
|
}
|
|
2423
2458
|
if (!options.force) return null;
|
|
@@ -2940,6 +2975,40 @@ var SelectionState = class {
|
|
|
2940
2975
|
return "changed";
|
|
2941
2976
|
}
|
|
2942
2977
|
};
|
|
2978
|
+
var SliderState = class {
|
|
2979
|
+
constructor(levels, selected, onSelect, onCancel) {
|
|
2980
|
+
this.levels = levels;
|
|
2981
|
+
this.selected = selected;
|
|
2982
|
+
this.onSelect = onSelect;
|
|
2983
|
+
this.onCancel = onCancel;
|
|
2984
|
+
}
|
|
2985
|
+
levels;
|
|
2986
|
+
selected;
|
|
2987
|
+
onSelect;
|
|
2988
|
+
onCancel;
|
|
2989
|
+
handleKey(input, key) {
|
|
2990
|
+
if (isCancel(input, key)) {
|
|
2991
|
+
this.onCancel();
|
|
2992
|
+
return "closed";
|
|
2993
|
+
}
|
|
2994
|
+
if (key.return) {
|
|
2995
|
+
const level = this.levels[this.selected];
|
|
2996
|
+
if (!level) return "ignored";
|
|
2997
|
+
this.onSelect(level);
|
|
2998
|
+
return "closed";
|
|
2999
|
+
}
|
|
3000
|
+
const last = this.levels.length - 1;
|
|
3001
|
+
let next = this.selected;
|
|
3002
|
+
if (key.leftArrow || key.downArrow) next = Math.max(0, this.selected - 1);
|
|
3003
|
+
else if (key.rightArrow || key.upArrow) next = Math.min(last, this.selected + 1);
|
|
3004
|
+
else if (key.home) next = 0;
|
|
3005
|
+
else if (key.end) next = last;
|
|
3006
|
+
else return "ignored";
|
|
3007
|
+
if (next === this.selected) return "ignored";
|
|
3008
|
+
this.selected = next;
|
|
3009
|
+
return "changed";
|
|
3010
|
+
}
|
|
3011
|
+
};
|
|
2943
3012
|
var ScrollState = class {
|
|
2944
3013
|
constructor(onClose) {
|
|
2945
3014
|
this.onClose = onClose;
|
|
@@ -3188,6 +3257,9 @@ function permissionKey(request, input, key) {
|
|
|
3188
3257
|
return "closed";
|
|
3189
3258
|
}
|
|
3190
3259
|
|
|
3260
|
+
// src/tui/ink/controller.ts
|
|
3261
|
+
import { effortLabel } from "@datalabrotterdam/nova-ai-agent/core/model-capabilities.js";
|
|
3262
|
+
|
|
3191
3263
|
// src/tui/ink/editor/prompt-editor.ts
|
|
3192
3264
|
var MAX_HISTORY = 200;
|
|
3193
3265
|
var PromptEditor = class {
|
|
@@ -3874,7 +3946,7 @@ var TuiController = class {
|
|
|
3874
3946
|
};
|
|
3875
3947
|
getView = () => this.view;
|
|
3876
3948
|
start() {
|
|
3877
|
-
void this.runner.
|
|
3949
|
+
void this.runner.prepareSession().then(() => this.refresh());
|
|
3878
3950
|
this.syncWindowTitle();
|
|
3879
3951
|
this.animationTimer = setInterval(() => this.syncWindowTitle(true), 120);
|
|
3880
3952
|
this.animationTimer.unref?.();
|
|
@@ -4021,8 +4093,8 @@ var TuiController = class {
|
|
|
4021
4093
|
"Models",
|
|
4022
4094
|
models.map((model) => ({
|
|
4023
4095
|
value: model.id,
|
|
4024
|
-
label: model.id,
|
|
4025
|
-
description: model.name
|
|
4096
|
+
label: model.thinking ? `${model.id} \u25C6` : model.id,
|
|
4097
|
+
description: [model.name, model.thinking ? "thinking" : null].filter(Boolean).join(" \xB7 ") || void 0
|
|
4026
4098
|
})),
|
|
4027
4099
|
"model-picker",
|
|
4028
4100
|
(model) => this.setModel(model)
|
|
@@ -4031,6 +4103,58 @@ var TuiController = class {
|
|
|
4031
4103
|
this.showText("Models", errorMessage(error, "Failed to list models."));
|
|
4032
4104
|
}
|
|
4033
4105
|
};
|
|
4106
|
+
effortName() {
|
|
4107
|
+
const level = this.runner.currentEffort();
|
|
4108
|
+
const efforts = level ? this.runner.modelEffortLevels() : [];
|
|
4109
|
+
return level ? effortLabel(level, efforts) : null;
|
|
4110
|
+
}
|
|
4111
|
+
/** The selected model's levels and current one, or why it has none. */
|
|
4112
|
+
async effortLevels() {
|
|
4113
|
+
let info;
|
|
4114
|
+
try {
|
|
4115
|
+
info = await this.runner.modelDetails();
|
|
4116
|
+
} catch (error) {
|
|
4117
|
+
return errorMessage(error, "Could not load the model list.");
|
|
4118
|
+
}
|
|
4119
|
+
const model = this.runner.model || "This model";
|
|
4120
|
+
if (!info?.efforts.length) {
|
|
4121
|
+
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.`;
|
|
4122
|
+
}
|
|
4123
|
+
return { efforts: info.efforts, current: this.runner.currentEffort() ?? info.efforts[0] };
|
|
4124
|
+
}
|
|
4125
|
+
/** /effort: a slider over the selected model's levels. */
|
|
4126
|
+
openEffort = async () => {
|
|
4127
|
+
const levels = await this.effortLevels();
|
|
4128
|
+
if (typeof levels === "string") {
|
|
4129
|
+
this.showText("Thinking effort", levels);
|
|
4130
|
+
return;
|
|
4131
|
+
}
|
|
4132
|
+
const state = new SliderState(
|
|
4133
|
+
levels.efforts.map((value) => ({ value, label: effortLabel(value, levels.efforts) })),
|
|
4134
|
+
Math.max(0, levels.efforts.indexOf(levels.current)),
|
|
4135
|
+
(item) => {
|
|
4136
|
+
this.closeOverlay();
|
|
4137
|
+
this.runner.setEffort(item.value);
|
|
4138
|
+
this.store.setState({ statusLine: `Thinking effort: ${item.label}.` });
|
|
4139
|
+
},
|
|
4140
|
+
this.closeOverlay
|
|
4141
|
+
);
|
|
4142
|
+
this.showOverlay({ kind: "slider", title: `Thinking effort \xB7 ${this.runner.model}`, state });
|
|
4143
|
+
};
|
|
4144
|
+
/** /effort <level>: by id ("high") or name ("Extra high", "off"). */
|
|
4145
|
+
setEffort = async (level) => {
|
|
4146
|
+
const levels = await this.effortLevels();
|
|
4147
|
+
if (typeof levels === "string") return levels;
|
|
4148
|
+
const wanted = level.trim().toLowerCase();
|
|
4149
|
+
const value = levels.efforts.find(
|
|
4150
|
+
(candidate) => candidate === wanted || effortLabel(candidate, levels.efforts).toLowerCase() === wanted
|
|
4151
|
+
);
|
|
4152
|
+
const names = levels.efforts.map((candidate) => effortLabel(candidate, levels.efforts).toLowerCase());
|
|
4153
|
+
if (!value) return `${this.runner.model} has these levels: ${names.join(", ")}.`;
|
|
4154
|
+
this.runner.setEffort(value);
|
|
4155
|
+
this.refresh();
|
|
4156
|
+
return `Thinking effort: ${effortLabel(value, levels.efforts)}.`;
|
|
4157
|
+
};
|
|
4034
4158
|
openPermissionModes = () => {
|
|
4035
4159
|
const modes = [
|
|
4036
4160
|
{ value: "ask", label: "ask", description: "Prompt before tools make changes" },
|
|
@@ -4084,9 +4208,13 @@ ${result.output}${result.truncated ? "\n\n[output truncated]" : ""}`
|
|
|
4084
4208
|
state: new ToolInspectorState(() => this.toolCalls(), this.closeOverlay)
|
|
4085
4209
|
});
|
|
4086
4210
|
};
|
|
4087
|
-
openMcpInspector = () => {
|
|
4088
|
-
|
|
4211
|
+
openMcpInspector = async () => {
|
|
4212
|
+
if (!this.runner.sessionLoaded) {
|
|
4213
|
+
this.showText("MCP servers", "Connecting MCP servers...");
|
|
4214
|
+
await this.runner.prepareSession();
|
|
4215
|
+
}
|
|
4089
4216
|
const connection = this.runner.mcpSessionStatus();
|
|
4217
|
+
const servers = connection.configured;
|
|
4090
4218
|
const configuredNames = new Set(servers.map((server) => server.name));
|
|
4091
4219
|
const configurationFailures = connection.failures.filter((failure) => !configuredNames.has(failure.serverName)).map((failure) => `${failure.serverName}
|
|
4092
4220
|
status: failed - ${failure.message}`);
|
|
@@ -4100,16 +4228,20 @@ status: failed - ${failure.message}`);
|
|
|
4100
4228
|
);
|
|
4101
4229
|
};
|
|
4102
4230
|
/** Every discovered skill with its state; switched-off skills stay listed. */
|
|
4103
|
-
openSkillInspector = () => {
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4231
|
+
openSkillInspector = async () => {
|
|
4232
|
+
let skills;
|
|
4233
|
+
try {
|
|
4234
|
+
skills = await this.runner.listSkills();
|
|
4235
|
+
} catch (error) {
|
|
4236
|
+
this.showText("Skills", errorMessage(error, "Could not list the skills."));
|
|
4237
|
+
return;
|
|
4238
|
+
}
|
|
4239
|
+
const on = skills.filter((skill) => skill.enabled).length;
|
|
4108
4240
|
this.showText(
|
|
4109
4241
|
`Skills (${on} on, ${skills.length - on} off)`,
|
|
4110
4242
|
skills.length ? [
|
|
4111
4243
|
...skills.map(
|
|
4112
|
-
(skill) => `${skill.name} [${skill.source}]${
|
|
4244
|
+
(skill) => `${skill.name} [${skill.bundledBy ? `bundled with ${skill.bundledBy}` : skill.source}]${skill.enabled ? "" : " (off)"}
|
|
4113
4245
|
${skill.description}
|
|
4114
4246
|
${skill.path}`
|
|
4115
4247
|
),
|
|
@@ -4118,17 +4250,15 @@ ${skill.path}`
|
|
|
4118
4250
|
);
|
|
4119
4251
|
};
|
|
4120
4252
|
/** /skills on|off <name> [--global]: applies from the next message. */
|
|
4121
|
-
switchSkill = (name, enabled, global) => {
|
|
4122
|
-
|
|
4123
|
-
if (!discoverSkills2(cwd).some((skill) => skill.name === name)) {
|
|
4124
|
-
return `No skill named "${name}". /skills lists them.`;
|
|
4125
|
-
}
|
|
4253
|
+
switchSkill = async (name, enabled, global) => {
|
|
4254
|
+
let skills;
|
|
4126
4255
|
try {
|
|
4127
|
-
|
|
4256
|
+
skills = await this.runner.setSkillEnabled(name, enabled, global);
|
|
4128
4257
|
} catch (error) {
|
|
4129
|
-
|
|
4258
|
+
const message = errorMessage(error, "Could not change the skill.");
|
|
4259
|
+
return message.startsWith("No skill named") ? `${message} /skills lists them.` : message;
|
|
4130
4260
|
}
|
|
4131
|
-
const stillOff = enabled &&
|
|
4261
|
+
const stillOff = enabled && skills.some((skill) => skill.name === name && !skill.enabled);
|
|
4132
4262
|
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.`;
|
|
4133
4263
|
};
|
|
4134
4264
|
openUsageInspector = async () => {
|
|
@@ -4182,8 +4312,8 @@ ${skill.path}`
|
|
|
4182
4312
|
this.runner.setInteractionMode(mode);
|
|
4183
4313
|
}
|
|
4184
4314
|
cycleInteractionMode() {
|
|
4185
|
-
const
|
|
4186
|
-
this.setInteractionMode(
|
|
4315
|
+
const modes = this.runner.modeIds;
|
|
4316
|
+
this.setInteractionMode(modes[(modes.indexOf(this.runner.interactionMode) + 1) % modes.length]);
|
|
4187
4317
|
}
|
|
4188
4318
|
appendAssistant(text) {
|
|
4189
4319
|
this.store.setState((state) => ({
|
|
@@ -4225,15 +4355,34 @@ ${skill.path}`
|
|
|
4225
4355
|
},
|
|
4226
4356
|
openSessionSwitcher: this.openSessions,
|
|
4227
4357
|
openModelPicker: () => void this.openModels(),
|
|
4358
|
+
hasThinkingLevels: () => this.runner.hasThinkingLevels(),
|
|
4359
|
+
openEffortPicker: () => this.openEffort(),
|
|
4360
|
+
setEffort: (level) => this.setEffort(level),
|
|
4228
4361
|
getModel: () => this.runner.model,
|
|
4229
4362
|
setModel: (model) => this.setModel(model),
|
|
4230
4363
|
getInteractionMode: () => this.runner.interactionMode,
|
|
4364
|
+
getInteractionModes: () => this.runner.modeIds,
|
|
4231
4365
|
setInteractionMode: (mode) => this.setInteractionMode(mode),
|
|
4232
4366
|
getPermissionMode: () => this.store.getState().permissionMode,
|
|
4233
4367
|
setPermissionMode: (mode) => this.setPermissionMode(mode),
|
|
4234
4368
|
openPermissionPicker: this.openPermissionModes,
|
|
4235
|
-
|
|
4236
|
-
|
|
4369
|
+
mcpSignIn: async (serverName, signIn) => {
|
|
4370
|
+
try {
|
|
4371
|
+
const status = await this.runner.mcpSignIn(serverName, signIn);
|
|
4372
|
+
this.refresh();
|
|
4373
|
+
const failure = status.failures.find((item) => item.serverName === serverName);
|
|
4374
|
+
if (!signIn) return `Signed out of "${serverName}".`;
|
|
4375
|
+
return failure ? `Signed in, but "${serverName}" did not connect: ${failure.message}` : `Signed in to "${serverName}"; its tools are available from the next message.`;
|
|
4376
|
+
} catch (error) {
|
|
4377
|
+
return errorMessage(error, signIn ? "Sign-in failed." : "Sign-out failed.");
|
|
4378
|
+
}
|
|
4379
|
+
},
|
|
4380
|
+
trustWorkspace: async () => {
|
|
4381
|
+
try {
|
|
4382
|
+
await this.runner.trustWorkspace();
|
|
4383
|
+
} catch (error) {
|
|
4384
|
+
return errorMessage(error, "Could not trust the workspace.");
|
|
4385
|
+
}
|
|
4237
4386
|
return "Workspace trusted: its allow rules apply now; MCP servers it declares start with the next session (/clear).";
|
|
4238
4387
|
},
|
|
4239
4388
|
openToolInspector: this.openToolInspector,
|
|
@@ -4334,7 +4483,7 @@ ${skill.path}`
|
|
|
4334
4483
|
const fdPath = resolveFdPath();
|
|
4335
4484
|
editor.setAutocompleteProvider(
|
|
4336
4485
|
new WorkspaceAutocompleteProvider(
|
|
4337
|
-
allCommands().map((command) => ({
|
|
4486
|
+
() => allCommands(this.commandContext).map((command) => ({
|
|
4338
4487
|
name: command.name,
|
|
4339
4488
|
description: command.description
|
|
4340
4489
|
})),
|
|
@@ -4375,6 +4524,7 @@ ${skill.path}`
|
|
|
4375
4524
|
overlay: this.overlay,
|
|
4376
4525
|
question: this.questionState(ui.pendingQuestion),
|
|
4377
4526
|
model: this.runner.model,
|
|
4527
|
+
effort: this.effortName(),
|
|
4378
4528
|
mcp: mcpCounts(this.runner),
|
|
4379
4529
|
expandedTools: this.expandedTools,
|
|
4380
4530
|
exiting: this.exiting,
|
|
@@ -4410,18 +4560,16 @@ ${skill.path}`
|
|
|
4410
4560
|
}
|
|
4411
4561
|
};
|
|
4412
4562
|
function formatMcpServer(server, status) {
|
|
4413
|
-
const
|
|
4414
|
-
|
|
4415
|
-
if (
|
|
4416
|
-
if ("command" in server) {
|
|
4563
|
+
const lines = [`${server.name} [${server.transport}]`];
|
|
4564
|
+
if (server.url) lines.push(`url: ${server.url}`);
|
|
4565
|
+
if (server.command) {
|
|
4417
4566
|
lines.push(`command: ${server.command}`);
|
|
4418
|
-
if (server.args
|
|
4419
|
-
if (server.env
|
|
4420
|
-
lines.push(`env: ${server.env.map((entry) => entry.name).join(", ")} (values hidden)`);
|
|
4567
|
+
if (server.args?.length) lines.push(`args: ${server.args.join(" ")}`);
|
|
4568
|
+
if (server.env?.length) lines.push(`env: ${server.env.join(", ")} (values hidden)`);
|
|
4421
4569
|
}
|
|
4422
4570
|
const failure = status.failures.find((item) => item.serverName === server.name);
|
|
4423
4571
|
lines.push(
|
|
4424
|
-
failure ? `status: failed - ${failure.message}` : status.connected.includes(server.name) ? "status: connected" : "status: configured; connection starts with the ACP session"
|
|
4572
|
+
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"
|
|
4425
4573
|
);
|
|
4426
4574
|
return lines.join("\n");
|
|
4427
4575
|
}
|
|
@@ -4460,6 +4608,25 @@ function Frame({ title, children, footer }) {
|
|
|
4460
4608
|
footer ? /* @__PURE__ */ jsx5(Text5, { color: palette.faint, wrap: "truncate-end", children: footer }) : null
|
|
4461
4609
|
] });
|
|
4462
4610
|
}
|
|
4611
|
+
function SliderDialog({
|
|
4612
|
+
title,
|
|
4613
|
+
state,
|
|
4614
|
+
isActive = true
|
|
4615
|
+
}) {
|
|
4616
|
+
useDialogInput((input, key) => state.handleKey(input, key), isActive);
|
|
4617
|
+
const current = state.levels[state.selected];
|
|
4618
|
+
return /* @__PURE__ */ jsxs5(Frame, { title, footer: "\u2190\u2192 adjust \xB7 Enter apply \xB7 Esc cancel", children: [
|
|
4619
|
+
/* @__PURE__ */ jsx5(Text5, { wrap: "truncate-end", children: state.levels.map((level, index) => {
|
|
4620
|
+
const selected = index === state.selected;
|
|
4621
|
+
return /* @__PURE__ */ jsxs5(Text5, { children: [
|
|
4622
|
+
index > 0 ? /* @__PURE__ */ jsx5(Text5, { color: palette.faint, children: " \u2500\u2500 " }) : null,
|
|
4623
|
+
/* @__PURE__ */ jsx5(Text5, { color: selected ? palette.primary : palette.faint, children: selected ? "\u25CF " : "\u25CB " }),
|
|
4624
|
+
/* @__PURE__ */ jsx5(Text5, { bold: selected, color: selected ? void 0 : palette.muted, children: level.label })
|
|
4625
|
+
] }, level.value);
|
|
4626
|
+
}) }),
|
|
4627
|
+
current?.description ? /* @__PURE__ */ jsx5(Text5, { color: palette.muted, children: current.description }) : null
|
|
4628
|
+
] });
|
|
4629
|
+
}
|
|
4463
4630
|
function SelectionDialog({
|
|
4464
4631
|
title,
|
|
4465
4632
|
state,
|
|
@@ -4848,7 +5015,7 @@ function App({ controller }) {
|
|
|
4848
5015
|
),
|
|
4849
5016
|
/* @__PURE__ */ jsxs6(Box6, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [
|
|
4850
5017
|
/* @__PURE__ */ jsx6(Surface, { controller, view, rows: Math.max(6, rows - 4), columns }),
|
|
4851
|
-
/* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, mcp: view.mcp, scroll: view.scroll })
|
|
5018
|
+
/* @__PURE__ */ jsx6(StatusLine, { ui: view.ui, model: view.model, effort: view.effort, mcp: view.mcp, scroll: view.scroll })
|
|
4852
5019
|
] })
|
|
4853
5020
|
] });
|
|
4854
5021
|
}
|
|
@@ -4891,6 +5058,7 @@ function Surface({
|
|
|
4891
5058
|
const overlay = view.overlay;
|
|
4892
5059
|
if (overlay?.kind === "selection")
|
|
4893
5060
|
return /* @__PURE__ */ jsx6(SelectionDialog, { title: overlay.title, state: overlay.state, height: rows });
|
|
5061
|
+
if (overlay?.kind === "slider") return /* @__PURE__ */ jsx6(SliderDialog, { title: overlay.title, state: overlay.state });
|
|
4894
5062
|
if (overlay?.kind === "text")
|
|
4895
5063
|
return /* @__PURE__ */ jsx6(
|
|
4896
5064
|
ScrollPanel,
|
|
@@ -4937,19 +5105,14 @@ function useWorking(view) {
|
|
|
4937
5105
|
}
|
|
4938
5106
|
|
|
4939
5107
|
// src/tui/index.ts
|
|
5108
|
+
import { workspaceNeedsTrust } from "@datalabrotterdam/nova-ai-agent/core/workspace-mcp.js";
|
|
4940
5109
|
async function runChat(options) {
|
|
4941
5110
|
const stored = readCredentials();
|
|
4942
5111
|
if (!stored) {
|
|
4943
5112
|
console.error("nova-ai isn't connected to a Nova AI account yet. Run `nova-ai login` first.");
|
|
4944
5113
|
return 1;
|
|
4945
5114
|
}
|
|
4946
|
-
const model = options.model ?? stored.defaultModel ?? process.env.NOVA_MODEL
|
|
4947
|
-
if (!model) {
|
|
4948
|
-
console.error("nova-ai has no model to use yet. Start it with one, for example:");
|
|
4949
|
-
console.error(" nova-ai --model <model-id> (or set NOVA_MODEL)");
|
|
4950
|
-
console.error("Then Ctrl+P in nova-ai switches and remembers the model.");
|
|
4951
|
-
return 1;
|
|
4952
|
-
}
|
|
5115
|
+
const model = options.model ?? stored.defaultModel ?? process.env.NOVA_MODEL;
|
|
4953
5116
|
const credentials = { ...stored, defaultModel: model };
|
|
4954
5117
|
const updateCheck = checkForUpdate().then((update) => withInstallability(update));
|
|
4955
5118
|
const updatedFrom = process.env[UPDATED_FROM_ENV];
|
|
@@ -4985,19 +5148,6 @@ async function runChat(options) {
|
|
|
4985
5148
|
}
|
|
4986
5149
|
return 0;
|
|
4987
5150
|
}
|
|
4988
|
-
async function pickDefaultModel(credentials) {
|
|
4989
|
-
try {
|
|
4990
|
-
const { data } = await createNovaClient2(credentials.apiKey).models.list({ limit: 100 });
|
|
4991
|
-
const model = resolveDefaultModel2(data, void 0);
|
|
4992
|
-
if (model) {
|
|
4993
|
-
saveDefaultModel2(model);
|
|
4994
|
-
console.log(`Using model ${model} (Ctrl+P switches).`);
|
|
4995
|
-
}
|
|
4996
|
-
return model;
|
|
4997
|
-
} catch {
|
|
4998
|
-
return void 0;
|
|
4999
|
-
}
|
|
5000
|
-
}
|
|
5001
5151
|
async function askToTrust(cwd) {
|
|
5002
5152
|
const prompt = createInterface2({ input: process.stdin, output: process.stdout });
|
|
5003
5153
|
try {
|
|
@@ -5007,7 +5157,7 @@ ${cwd}`);
|
|
|
5007
5157
|
console.log("Trusting it starts those servers and applies those rules. Only trust code you know.");
|
|
5008
5158
|
const answer = (await prompt.question("Trust this workspace? [y/N] ")).trim().toLowerCase();
|
|
5009
5159
|
if (answer === "y" || answer === "yes") {
|
|
5010
|
-
|
|
5160
|
+
setWorkspaceTrusted(cwd, true);
|
|
5011
5161
|
} else {
|
|
5012
5162
|
console.log("Not trusted: its MCP servers stay off and its allow rules are ignored. Use /trust later to change this.");
|
|
5013
5163
|
}
|