@mafdet/cli 0.4.4 → 0.4.6
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 +81 -0
- package/README.md +4 -6
- package/README.zh-CN.md +4 -3
- package/dist/agent/command-policy.d.ts.map +1 -1
- package/dist/agent/command-policy.js +19 -6
- package/dist/agent/context-budget.d.ts +19 -1
- package/dist/agent/context-budget.d.ts.map +1 -1
- package/dist/agent/context-budget.js +87 -12
- package/dist/agent/loop.d.ts +16 -1
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +34 -2
- package/dist/agent/session.d.ts +14 -0
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +15 -4
- package/dist/api/models.d.ts.map +1 -1
- package/dist/api/models.js +28 -2
- package/dist/api/sse-parser.d.ts +15 -0
- package/dist/api/sse-parser.d.ts.map +1 -1
- package/dist/api/sse-parser.js +39 -3
- package/dist/commands/agent.d.ts.map +1 -1
- package/dist/commands/agent.js +88 -30
- package/dist/commands/sessions.d.ts.map +1 -1
- package/dist/commands/sessions.js +45 -10
- package/dist/output/money.d.ts +9 -0
- package/dist/output/money.d.ts.map +1 -1
- package/dist/output/money.js +14 -0
- package/dist/sessions/index-file.d.ts +2 -0
- package/dist/sessions/index-file.d.ts.map +1 -1
- package/dist/sessions/index-file.js +3 -0
- package/dist/sessions/open.d.ts.map +1 -1
- package/dist/sessions/open.js +3 -1
- package/dist/sessions/resume.d.ts +1 -0
- package/dist/sessions/resume.d.ts.map +1 -1
- package/dist/sessions/resume.js +5 -3
- package/dist/tui/agent-surface.d.ts.map +1 -1
- package/dist/tui/agent-surface.js +18 -12
- package/dist/tui/app.d.ts +14 -0
- package/dist/tui/app.d.ts.map +1 -1
- package/dist/tui/app.js +45 -11
- package/dist/tui/picker.d.ts.map +1 -1
- package/dist/tui/picker.js +2 -1
- package/dist/tui/session-centre.d.ts +5 -0
- package/dist/tui/session-centre.d.ts.map +1 -1
- package/dist/tui/session-centre.js +14 -4
- package/dist/tui/start-agent.d.ts +28 -0
- package/dist/tui/start-agent.d.ts.map +1 -1
- package/dist/tui/start-agent.js +39 -9
- package/package.json +1 -1
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// different models, tools, approvals or costs, because there is only one
|
|
7
7
|
// implementation of all four. What differs is the drawing.
|
|
8
8
|
import { runSession, } from "../agent/session.js";
|
|
9
|
-
import {
|
|
9
|
+
import { formatUsdWithUnpriced } from "../output/money.js";
|
|
10
10
|
import { tuiApprover } from "./approval.js";
|
|
11
11
|
import { palette } from "./theme.js";
|
|
12
12
|
import { renderCard, ToolCardLog } from "./tool-card.js";
|
|
@@ -72,8 +72,17 @@ export async function runAgentSurface(options) {
|
|
|
72
72
|
// Said out loud: from here the model can no longer see those results,
|
|
73
73
|
// which is the sort of thing that otherwise looks like it forgot.
|
|
74
74
|
// The saved transcript still has them - only what we send got shorter.
|
|
75
|
+
// R-2 · Say what was actually dropped. A conversation with no tools in
|
|
76
|
+
// it drops turns and zero exchanges, and this used to report "0 earlier
|
|
77
|
+
// tool exchanges" - a true statement that described nothing.
|
|
78
|
+
const parts = [];
|
|
79
|
+
if (event.exchanges > 0) {
|
|
80
|
+
parts.push(`${event.exchanges} earlier tool exchange(s)`);
|
|
81
|
+
}
|
|
82
|
+
if (event.turns > 0)
|
|
83
|
+
parts.push(`${event.turns} earlier turn(s)`);
|
|
75
84
|
options.stdout.write(dim(` context near the limit (${event.promptTokens} tokens) - dropped ` +
|
|
76
|
-
`${
|
|
85
|
+
`${parts.join(" and ") || "nothing"} from what is sent\n`));
|
|
77
86
|
}
|
|
78
87
|
};
|
|
79
88
|
const sessionOptions = {
|
|
@@ -90,16 +99,13 @@ export async function runAgentSurface(options) {
|
|
|
90
99
|
// Checkpoint between goals rather than per event: the listing only needs
|
|
91
100
|
// to be current when someone might go and look at it.
|
|
92
101
|
options.recorder?.checkpoint();
|
|
93
|
-
|
|
94
|
-
// The user's own words never appear in the agent event stream, so they
|
|
95
|
-
// are recorded here. Slash commands are control, not conversation.
|
|
96
|
-
if (line !== undefined &&
|
|
97
|
-
line.trim().length > 0 &&
|
|
98
|
-
!line.startsWith("/")) {
|
|
99
|
-
options.recorder?.goal(line);
|
|
100
|
-
}
|
|
101
|
-
return line;
|
|
102
|
+
return promptLine(io, "> ");
|
|
102
103
|
},
|
|
104
|
+
// T-1 · Recording the goal belongs to the session loop now, so both
|
|
105
|
+
// surfaces get it from the same place instead of one remembering.
|
|
106
|
+
...(options.recorder === undefined
|
|
107
|
+
? {}
|
|
108
|
+
: { onGoal: (goal) => options.recorder?.goal(goal) }),
|
|
103
109
|
onEvent,
|
|
104
110
|
};
|
|
105
111
|
// Installed after the spread, on purpose - see getSignal above.
|
|
@@ -124,7 +130,7 @@ export function renderSummary(summary, capabilities) {
|
|
|
124
130
|
dim(` model requests ${summary.modelRequests}`),
|
|
125
131
|
dim(` tool runs ${summary.toolRuns}`),
|
|
126
132
|
dim(` tokens ${summary.promptTokens} in / ${summary.completionTokens} out`),
|
|
127
|
-
dim(` estimated cost ${
|
|
133
|
+
dim(` estimated cost ${formatUsdWithUnpriced(summary.costMicroUsd, summary.unpricedRequests)}`),
|
|
128
134
|
"",
|
|
129
135
|
].join("\n") + "\n");
|
|
130
136
|
}
|
package/dist/tui/app.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ExitCode } from "../output/exit-codes.js";
|
|
2
2
|
import type { CommandIo } from "../commands/runtime.js";
|
|
3
|
+
import { type TerminalCapabilities } from "./theme.js";
|
|
3
4
|
import { type GuardHost } from "./terminal.js";
|
|
5
|
+
import { type WidgetIo } from "./widgets.js";
|
|
4
6
|
export interface TuiOptions {
|
|
5
7
|
readonly version: string;
|
|
6
8
|
readonly workspace: string;
|
|
@@ -22,4 +24,16 @@ export interface TuiOptions {
|
|
|
22
24
|
readonly argv: readonly string[];
|
|
23
25
|
}
|
|
24
26
|
export declare function runTui(options: TuiOptions): Promise<ExitCode>;
|
|
27
|
+
interface ChooseOptions {
|
|
28
|
+
readonly io: WidgetIo;
|
|
29
|
+
readonly commandIo: CommandIo;
|
|
30
|
+
readonly apiKey: string;
|
|
31
|
+
readonly capabilities: TerminalCapabilities;
|
|
32
|
+
readonly stdout: {
|
|
33
|
+
write(text: string): unknown;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** The model the whole precedence chain settles on: flag > env > project > user. */
|
|
37
|
+
export declare function resolvedDefaultModel(options: ChooseOptions): string | undefined;
|
|
38
|
+
export {};
|
|
25
39
|
//# sourceMappingURL=app.d.ts.map
|
package/dist/tui/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/tui/app.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/tui/app.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAYxD,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAwB,KAAK,SAAS,EAAE,MAAM,eAAe,CAAC;AAerE,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAErD,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClD,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,UAAU,GAAG;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACzE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;IAC3D,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CAClC;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,QAAQ,CAAC,CAgLnE;AAED,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;CACnD;AAsBD,oFAAoF;AACpF,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,aAAa,GACrB,MAAM,GAAG,SAAS,CAYpB"}
|
package/dist/tui/app.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { ExitCode } from "../output/exit-codes.js";
|
|
9
9
|
import { loadConfig } from "../config/load.js";
|
|
10
|
-
import { loadCatalogue, saveDefaultModel } from "./adapters/models.js";
|
|
10
|
+
import { loadCatalogue, saveDefaultModel, } from "./adapters/models.js";
|
|
11
11
|
import { renderBanner } from "./banner.js";
|
|
12
12
|
import { keyboard } from "./input.js";
|
|
13
13
|
import { FORCED_EXIT_NOTICE, InterruptTracker } from "./interrupt.js";
|
|
@@ -110,18 +110,21 @@ export async function runTui(options) {
|
|
|
110
110
|
options.stdout.write("commands that work without an interactive session.\n");
|
|
111
111
|
return ExitCode.Cancelled;
|
|
112
112
|
}
|
|
113
|
-
const
|
|
113
|
+
const chooseOptions = {
|
|
114
114
|
io,
|
|
115
115
|
commandIo: options.commandIo,
|
|
116
116
|
apiKey: auth.apiKey,
|
|
117
117
|
capabilities,
|
|
118
118
|
stdout: options.stdout,
|
|
119
|
-
}
|
|
120
|
-
|
|
119
|
+
};
|
|
120
|
+
const catalogue = await loadModels(chooseOptions);
|
|
121
|
+
if (catalogue === undefined)
|
|
121
122
|
return ExitCode.Cancelled;
|
|
122
123
|
options.stdout.write(renderWorkspaceSummary(inspectWorkspace(options.workspace, existsSync), capabilities));
|
|
123
|
-
//
|
|
124
|
-
//
|
|
124
|
+
// R-9 · The session comes first. Resuming runs on the model the session
|
|
125
|
+
// recorded, so asking for one before knowing whether we are resuming made
|
|
126
|
+
// the user choose - and possibly save as their default - a model the run
|
|
127
|
+
// then ignored, after printing it in the banner as though it were in use.
|
|
125
128
|
const sessionOptions = parseSessionOptions(options.argv);
|
|
126
129
|
let resumeId = sessionOptions.resumeId;
|
|
127
130
|
if (resumeId === undefined && !sessionOptions.noHistory) {
|
|
@@ -131,6 +134,17 @@ export async function runTui(options) {
|
|
|
131
134
|
if (choice.kind === "resume")
|
|
132
135
|
resumeId = choice.sessionId;
|
|
133
136
|
}
|
|
137
|
+
// Only a new conversation needs a model chosen for it.
|
|
138
|
+
const chosen = resumeId === undefined
|
|
139
|
+
? await chooseModel(chooseOptions, catalogue)
|
|
140
|
+
: {
|
|
141
|
+
model: catalogue.agentReady[0] ?? catalogue.chat[0],
|
|
142
|
+
catalogue: catalogue.chat,
|
|
143
|
+
agentCapable: catalogue.agentReady.map((m) => m.modelId),
|
|
144
|
+
};
|
|
145
|
+
if (chosen === undefined || chosen.model === undefined) {
|
|
146
|
+
return ExitCode.Cancelled;
|
|
147
|
+
}
|
|
134
148
|
return await startAgent({
|
|
135
149
|
io,
|
|
136
150
|
capabilities,
|
|
@@ -160,7 +174,7 @@ export async function runTui(options) {
|
|
|
160
174
|
}
|
|
161
175
|
}
|
|
162
176
|
/** The model the whole precedence chain settles on: flag > env > project > user. */
|
|
163
|
-
function resolvedDefaultModel(options) {
|
|
177
|
+
export function resolvedDefaultModel(options) {
|
|
164
178
|
try {
|
|
165
179
|
return loadConfig({
|
|
166
180
|
env: options.commandIo.env,
|
|
@@ -174,7 +188,15 @@ function resolvedDefaultModel(options) {
|
|
|
174
188
|
return options.commandIo.env?.MAFDET_MODEL;
|
|
175
189
|
}
|
|
176
190
|
}
|
|
177
|
-
|
|
191
|
+
/**
|
|
192
|
+
* R-9 · Load and vet the catalogue, without choosing anything yet.
|
|
193
|
+
*
|
|
194
|
+
* Split from the picker because resuming does not need a choice: the session
|
|
195
|
+
* runs on the model it recorded. Asking for one first made the user pick - and
|
|
196
|
+
* possibly "remember" - a model that the run then ignored, and printed it in
|
|
197
|
+
* the banner as though it were in use.
|
|
198
|
+
*/
|
|
199
|
+
async function loadModels(options) {
|
|
178
200
|
const { dim, bold } = palette(options.capabilities);
|
|
179
201
|
options.stdout.write(dim(" loading the model catalogue...\n"));
|
|
180
202
|
let catalogue;
|
|
@@ -202,13 +224,25 @@ async function chooseModel(options) {
|
|
|
202
224
|
return undefined;
|
|
203
225
|
}
|
|
204
226
|
if (catalogue.agentReady.length === 0) {
|
|
227
|
+
// R-1 · Refuse, rather than warn and open the gate wider.
|
|
228
|
+
//
|
|
229
|
+
// This printed a notice and then offered *every* chat model, because
|
|
230
|
+
// `agentOnly` was `agentReady.length > 0` - so the one case with nothing
|
|
231
|
+
// safe to offer was the case that offered everything. The whole of this
|
|
232
|
+
// surface is an agent session, and `mafdet agent` already refuses an
|
|
233
|
+
// unverified model outright; the two must not disagree about admission.
|
|
205
234
|
options.stdout.write(noAgentModelsNotice(catalogue.chat.length, options.capabilities));
|
|
235
|
+
return undefined;
|
|
206
236
|
}
|
|
237
|
+
return catalogue;
|
|
238
|
+
}
|
|
239
|
+
async function chooseModel(options, catalogue) {
|
|
240
|
+
const { dim } = palette(options.capabilities);
|
|
207
241
|
const picked = await pickModel(options.io, {
|
|
208
242
|
models: catalogue.chat,
|
|
209
|
-
// Fail closed:
|
|
210
|
-
//
|
|
211
|
-
agentOnly:
|
|
243
|
+
// Fail closed: only agent-capable models are ever offered here. The
|
|
244
|
+
// empty case is refused in loadModels, so this is never a way in.
|
|
245
|
+
agentOnly: true,
|
|
212
246
|
// F-005 · The resolved model, not just the environment variable. After
|
|
213
247
|
// "Remember X as your default?" wrote X to the user config, the next
|
|
214
248
|
// launch still highlighted whatever MAFDET_MODEL said - usually nothing -
|
package/dist/tui/picker.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../src/tui/picker.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAW7C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAC7B,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,UAAU,CAAC,CAiHrB;AA6BD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,oBAAoB,GACjC,MAAM,
|
|
1
|
+
{"version":3,"file":"picker.d.ts","sourceRoot":"","sources":["../../src/tui/picker.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEtD,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACrC,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAW7C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAC;IACtC,QAAQ,CAAC,KAAK,CAAC,EAAE,aAAa,CAAC;CAChC;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;IAC1C,8CAA8C;IAC9C,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,iDAAiD;IACjD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,wBAAsB,SAAS,CAC7B,EAAE,EAAE,QAAQ,EACZ,OAAO,EAAE,aAAa,GACrB,OAAO,CAAC,UAAU,CAAC,CAiHrB;AA6BD;;;GAGG;AACH,wBAAgB,mBAAmB,CACjC,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAaR;AAED,iEAAiE;AACjE,YAAY,EAAE,GAAG,EAAE,CAAC"}
|
package/dist/tui/picker.js
CHANGED
|
@@ -138,7 +138,8 @@ export function noAgentModelsNotice(chatCount, capabilities) {
|
|
|
138
138
|
return ([
|
|
139
139
|
"",
|
|
140
140
|
bold(" No model on this key has verified tool calling."),
|
|
141
|
-
dim(
|
|
141
|
+
dim(" The agent needs one, so there is no session to open."),
|
|
142
|
+
dim(` ${chatCount} chat model(s) still work: try 'mafdet chat'.`),
|
|
142
143
|
dim(" Run 'mafdet models list --agent' to see which models qualify,"),
|
|
143
144
|
dim(" or check the key's model permissions in the console."),
|
|
144
145
|
"",
|
|
@@ -10,6 +10,11 @@ export type SessionChoice = {
|
|
|
10
10
|
readonly kind: "cancelled";
|
|
11
11
|
};
|
|
12
12
|
export declare function recentSessions(root: string): readonly IndexEntry[];
|
|
13
|
+
/**
|
|
14
|
+
* R-4 · Every string here came off disk, and this list is drawn on a terminal
|
|
15
|
+
* by definition. A title carrying an OSC sequence would retitle the window of
|
|
16
|
+
* anyone who merely opened the picker - before choosing anything.
|
|
17
|
+
*/
|
|
13
18
|
export declare function describeSession(entry: IndexEntry, capabilities: TerminalCapabilities): string;
|
|
14
19
|
export declare function chooseSession(io: WidgetIo, root: string): Promise<SessionChoice>;
|
|
15
20
|
//# sourceMappingURL=session-centre.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-centre.d.ts","sourceRoot":"","sources":["../../src/tui/session-centre.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"session-centre.d.ts","sourceRoot":"","sources":["../../src/tui/session-centre.ts"],"names":[],"mappings":"AAQA,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvE,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAAU,KAAK,QAAQ,EAAE,MAAM,cAAc,CAAC;AAKrD,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CAAE,GACxB;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACvD;IAAE,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAEnC,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,UAAU,EAAE,CAQlE;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,UAAU,EACjB,YAAY,EAAE,oBAAoB,GACjC,MAAM,CAYR;AAOD,wBAAsB,aAAa,CACjC,EAAE,EAAE,QAAQ,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,aAAa,CAAC,CAoBxB"}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// Offered only when there is something to resume: a menu whose only entry is
|
|
4
4
|
// "New conversation" is a keystroke charged for nothing. Everything here reads
|
|
5
5
|
// the local index and sends no request, so browsing costs nothing (plan 20.2).
|
|
6
|
+
import { stripAnsi } from "../agent/command-policy.js";
|
|
6
7
|
import { formatUsd } from "../output/money.js";
|
|
7
8
|
import { loadIndex } from "../sessions/index-file.js";
|
|
8
9
|
import { palette } from "./theme.js";
|
|
@@ -19,12 +20,21 @@ export function recentSessions(root) {
|
|
|
19
20
|
return [];
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* R-4 · Every string here came off disk, and this list is drawn on a terminal
|
|
25
|
+
* by definition. A title carrying an OSC sequence would retitle the window of
|
|
26
|
+
* anyone who merely opened the picker - before choosing anything.
|
|
27
|
+
*/
|
|
22
28
|
export function describeSession(entry, capabilities) {
|
|
23
29
|
const parts = [
|
|
24
|
-
entry.model_id,
|
|
25
|
-
workspaceName(entry.workspace),
|
|
30
|
+
stripAnsi(entry.model_id),
|
|
31
|
+
stripAnsi(workspaceName(entry.workspace)),
|
|
26
32
|
`${entry.model_requests} request(s)`,
|
|
27
|
-
|
|
33
|
+
// T-8 · `sessions list/show/usage` learned to say "unknown"; this list
|
|
34
|
+
// did not, so the same session read as free in the picker.
|
|
35
|
+
entry.unpriced_requests
|
|
36
|
+
? `~${formatUsd(entry.estimated_charge_micro_usd)} + ${entry.unpriced_requests} unpriced`
|
|
37
|
+
: `~${formatUsd(entry.estimated_charge_micro_usd)}`,
|
|
28
38
|
];
|
|
29
39
|
return palette(capabilities).dim(parts.join(" - "));
|
|
30
40
|
}
|
|
@@ -42,7 +52,7 @@ export async function chooseSession(io, root) {
|
|
|
42
52
|
{ value: "", label: "New conversation" },
|
|
43
53
|
...recent.map((entry) => ({
|
|
44
54
|
value: entry.session_id,
|
|
45
|
-
label: entry.title,
|
|
55
|
+
label: stripAnsi(entry.title),
|
|
46
56
|
detail: stripStyles(describeSession(entry, io.capabilities)),
|
|
47
57
|
})),
|
|
48
58
|
],
|
|
@@ -73,5 +73,33 @@ export interface StartAgentOptions {
|
|
|
73
73
|
*/
|
|
74
74
|
readonly getSignal?: () => AbortSignal | undefined;
|
|
75
75
|
}
|
|
76
|
+
export interface RunContextInput {
|
|
77
|
+
/** The metadata of a resumed session, or undefined for a new one. */
|
|
78
|
+
readonly resumed?: {
|
|
79
|
+
readonly workspace: string;
|
|
80
|
+
readonly model_id: string;
|
|
81
|
+
};
|
|
82
|
+
readonly currentWorkspace: string;
|
|
83
|
+
readonly currentModel: ResolvedModel;
|
|
84
|
+
readonly catalogue: readonly ResolvedModel[];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* F-003 · Where a run happens and which model it uses.
|
|
88
|
+
*
|
|
89
|
+
* A resumed session runs where it was recorded, on the model it was recorded
|
|
90
|
+
* with. It used to run in the current directory on the model picked moments
|
|
91
|
+
* earlier - so continuing project A's conversation from project B pointed the
|
|
92
|
+
* agent at B's files, and with `--allow-write` that meant editing the wrong
|
|
93
|
+
* repository.
|
|
94
|
+
*
|
|
95
|
+
* Falls back to the current model only when the catalogue has no entry for the
|
|
96
|
+
* recorded one. That cannot happen on a run that got this far - an uncallable
|
|
97
|
+
* model is a blocker and blockers now stop the run - but the fallback keeps
|
|
98
|
+
* this total rather than throwing on a case the type system allows.
|
|
99
|
+
*/
|
|
100
|
+
export declare function effectiveRunContext(input: RunContextInput): {
|
|
101
|
+
workspace: string;
|
|
102
|
+
model: ResolvedModel;
|
|
103
|
+
};
|
|
76
104
|
export declare function startAgent(options: StartAgentOptions): Promise<ExitCode>;
|
|
77
105
|
//# sourceMappingURL=start-agent.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-agent.d.ts","sourceRoot":"","sources":["../../src/tui/start-agent.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEtE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"start-agent.d.ts","sourceRoot":"","sources":["../../src/tui/start-agent.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAgB,KAAK,SAAS,EAAE,MAAM,wBAAwB,CAAC;AAEtE,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAgBnD,OAAO,EAAW,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE7C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;CAC7C;AAED;;;;;GAKG;AACH,kEAAkE;AAClE,2DAA2D;AAC3D,eAAO,MAAM,SAAS,iHAQZ,CAAC;AASX,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAE9C;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAgB/D;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,cAAc,CAiB3E;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,gBAAgB,CAgB1E;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,YAAY,EAAE,oBAAoB,CAAC;IAC5C,QAAQ,CAAC,MAAM,EAAE;QAAE,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAC;IAClD,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC9C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAChD,QAAQ,CAAC,WAAW,EAAE,gBAAgB,CAAC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,WAAW,GAAG,SAAS,CAAC;CACpD;AAED,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,QAAQ,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC7E,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,YAAY,EAAE,aAAa,CAAC;IACrC,QAAQ,CAAC,SAAS,EAAE,SAAS,aAAa,EAAE,CAAC;CAC9C;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,eAAe,GAAG;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,aAAa,CAAC;CACtB,CAUA;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC,QAAQ,CAAC,CAgKnB"}
|
package/dist/tui/start-agent.js
CHANGED
|
@@ -15,6 +15,7 @@ import { keyDigest } from "../security/redact.js";
|
|
|
15
15
|
import { SessionRecorder } from "../sessions/recorder.js";
|
|
16
16
|
import { SessionWriter } from "../sessions/store.js";
|
|
17
17
|
import { NOTICE_FILE, decideRecording, firstSaveNotice, } from "../sessions/policy.js";
|
|
18
|
+
import { stripAnsi } from "../agent/command-policy.js";
|
|
18
19
|
import { describeResume, openForResume } from "../sessions/open.js";
|
|
19
20
|
import { describeBlocker } from "../sessions/resume.js";
|
|
20
21
|
import { runAgentSurface } from "./agent-surface.js";
|
|
@@ -118,6 +119,30 @@ export function parsePermissions(argv) {
|
|
|
118
119
|
}
|
|
119
120
|
return { allowWrite, allowedCommands };
|
|
120
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* F-003 · Where a run happens and which model it uses.
|
|
124
|
+
*
|
|
125
|
+
* A resumed session runs where it was recorded, on the model it was recorded
|
|
126
|
+
* with. It used to run in the current directory on the model picked moments
|
|
127
|
+
* earlier - so continuing project A's conversation from project B pointed the
|
|
128
|
+
* agent at B's files, and with `--allow-write` that meant editing the wrong
|
|
129
|
+
* repository.
|
|
130
|
+
*
|
|
131
|
+
* Falls back to the current model only when the catalogue has no entry for the
|
|
132
|
+
* recorded one. That cannot happen on a run that got this far - an uncallable
|
|
133
|
+
* model is a blocker and blockers now stop the run - but the fallback keeps
|
|
134
|
+
* this total rather than throwing on a case the type system allows.
|
|
135
|
+
*/
|
|
136
|
+
export function effectiveRunContext(input) {
|
|
137
|
+
if (input.resumed === undefined) {
|
|
138
|
+
return { workspace: input.currentWorkspace, model: input.currentModel };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
workspace: input.resumed.workspace,
|
|
142
|
+
model: input.catalogue.find((m) => m.modelId === input.resumed?.model_id) ??
|
|
143
|
+
input.currentModel,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
121
146
|
export async function startAgent(options) {
|
|
122
147
|
const { dim, bold } = palette(options.capabilities);
|
|
123
148
|
// The TUI is interactive by construction, so `--allow-write` can be honoured
|
|
@@ -128,11 +153,6 @@ export async function startAgent(options) {
|
|
|
128
153
|
...options.commandIo,
|
|
129
154
|
env: { ...options.commandIo.env, MAFDET_API_KEY: options.apiKey },
|
|
130
155
|
});
|
|
131
|
-
options.stdout.write(dim(`\n ${options.model.modelId} - ${mode}` +
|
|
132
|
-
(hasCommands
|
|
133
|
-
? ` - commands: ${options.permissions.allowedCommands.join(", ")}`
|
|
134
|
-
: "") +
|
|
135
|
-
"\n type a goal, or /help for the session commands\n\n"));
|
|
136
156
|
const opened = options.session?.resumeId
|
|
137
157
|
? openForResume({
|
|
138
158
|
root: sessionsDir(options.commandIo.env ?? process.env),
|
|
@@ -174,10 +194,12 @@ export async function startAgent(options) {
|
|
|
174
194
|
// was recorded with. Continuing project A's conversation against project B's
|
|
175
195
|
// files is how an agent with write access edits the wrong repository - and
|
|
176
196
|
// it used to be the default, because the workspace came from the cwd.
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
:
|
|
197
|
+
const { workspace: effectiveWorkspace, model: effectiveModel } = effectiveRunContext({
|
|
198
|
+
resumed: opened?.metadata,
|
|
199
|
+
currentWorkspace: options.workspace,
|
|
200
|
+
currentModel: options.model,
|
|
201
|
+
catalogue: options.catalogue ?? [],
|
|
202
|
+
});
|
|
181
203
|
let workspace;
|
|
182
204
|
try {
|
|
183
205
|
workspace = Workspace.open(effectiveWorkspace);
|
|
@@ -194,6 +216,14 @@ export async function startAgent(options) {
|
|
|
194
216
|
if (opened !== undefined && effectiveWorkspace !== options.workspace) {
|
|
195
217
|
options.stdout.write(dim(` continuing in ${effectiveWorkspace}, where it was recorded\n`));
|
|
196
218
|
}
|
|
219
|
+
// R-9 · Printed here, not before the session was resolved: on a resume the
|
|
220
|
+
// model in use is the one the session recorded, and announcing the model
|
|
221
|
+
// picked beforehand named something that was never going to be used.
|
|
222
|
+
options.stdout.write(dim(`\n ${stripAnsi(effectiveModel.modelId)} - ${mode}` +
|
|
223
|
+
(hasCommands
|
|
224
|
+
? ` - commands: ${options.permissions.allowedCommands.join(", ")}`
|
|
225
|
+
: "") +
|
|
226
|
+
"\n type a goal, or /help for the session commands\n\n"));
|
|
197
227
|
const recorder = opened !== undefined
|
|
198
228
|
? new SessionRecorder({
|
|
199
229
|
writer: opened.writer,
|