@quatcode/agent 0.2.8 → 0.2.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/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/dist/gate.d.ts +4 -1
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +47 -20
- package/dist/gate.js.map +1 -1
- package/dist/persona.d.ts +26 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +186 -0
- package/dist/persona.js.map +1 -0
- package/dist/picker.d.ts +4 -0
- package/dist/picker.d.ts.map +1 -1
- package/dist/picker.js +30 -4
- package/dist/picker.js.map +1 -1
- package/dist/screen.js +1 -1
- package/dist/screen.js.map +1 -1
- package/dist/store.d.ts +19 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +23 -0
- package/dist/store.js.map +1 -1
- package/dist/theme.d.ts +16 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +35 -5
- package/dist/theme.js.map +1 -1
- package/dist/tui.d.ts +7 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +139 -14
- package/dist/tui.js.map +1 -1
- package/dist/turn.d.ts +2 -0
- package/dist/turn.d.ts.map +1 -1
- package/dist/turn.js +7 -4
- package/dist/turn.js.map +1 -1
- package/package.json +3 -3
package/dist/tui.js
CHANGED
|
@@ -12,10 +12,11 @@ import { TurnController, fmtCost, fmtDuration, fmtTokens, toolArg, toolResultBlo
|
|
|
12
12
|
import { renderBanner } from "./logo.js";
|
|
13
13
|
import { renderMarkdown, wrapSegments } from "./markdown.js";
|
|
14
14
|
import { ADD_PROVIDER, allProviders, buildProvider, defaultModelFor, effortsForModel, hasKey, keyEnvFor, probeProviderModels, providerEnvKey, providerHasModel, providerMeta, providerSlug, registerCustomProvider, removeCustomProvider, resolveProviderForModel, } from "./models.js";
|
|
15
|
-
import { activeThemeId, bg, bold, buildColorRemap, clipAnsi, fg, getThemes, glyphs, padEndTo, palette, remapColors, setTheme, stringWidth, stripAnsi, tone, toolGlyph, } from "./theme.js";
|
|
15
|
+
import { ASCII_MODE, activeThemeId, bg, bold, buildColorRemap, clipAnsi, fg, getThemes, glyphs, mix, padEndTo, palette, pulse, remapColors, setTheme, shimmer, stringWidth, stripAnsi, tone, toolGlyph, } from "./theme.js";
|
|
16
16
|
import { listSessions, listSessionsForDir, loadConfig, makeSessionId, saveConfig, saveSession, deleteSession } from "./store.js";
|
|
17
|
-
import { configFile, conversationKey, deleteConversation, displayTitle, latestPerConversation, messagesForStorage, sameDir, sortConversations, updateConversation } from "./store.js";
|
|
17
|
+
import { appendAudit, auditInput, configFile, conversationKey, deleteConversation, displayTitle, latestPerConversation, messagesForStorage, sameDir, sortConversations, updateConversation } from "./store.js";
|
|
18
18
|
import { MCP_USAGE, isMcpUrl, mcpInstructionsSection, parseMcpAdd } from "./mcp.js";
|
|
19
|
+
import { AUTO, PERSONA_SETTINGS, personaSection, personaSummary, } from "./persona.js";
|
|
19
20
|
import { copyToClipboard, readClipboardImage, readClipboardText, } from "./clipboard.js";
|
|
20
21
|
import { createLoadSkillTool, fetchSkillFromUrl, importSkillFile, listSkills, parseSkill, removeSkill, saveSkill, skillsRoot, skillsSystemSection, } from "./skills.js";
|
|
21
22
|
import { VERSION } from "./version.js";
|
|
@@ -34,6 +35,7 @@ const COMMAND_INFO = [
|
|
|
34
35
|
{ name: "/timeline", desc: "jump to a sent message" },
|
|
35
36
|
{ name: "/undo", desc: "take back the last message" },
|
|
36
37
|
{ name: "/verbose", desc: "toggle full tool output" },
|
|
38
|
+
{ name: "/settings", desc: "tune the agent's persona: character, tone, verbosity…" },
|
|
37
39
|
{ name: "/skills", args: "[add|remove|list]", desc: "manage agent skills" },
|
|
38
40
|
{ name: "/mcp", args: "[list|add|remove]", desc: "manage MCP servers" },
|
|
39
41
|
{ name: "/theme", args: "[id]", desc: "change the color theme" },
|
|
@@ -75,6 +77,7 @@ class TuiApp {
|
|
|
75
77
|
model = "";
|
|
76
78
|
effort = null;
|
|
77
79
|
config = {};
|
|
80
|
+
persona = {};
|
|
78
81
|
summary = null;
|
|
79
82
|
skills = [];
|
|
80
83
|
system = "";
|
|
@@ -185,6 +188,7 @@ class TuiApp {
|
|
|
185
188
|
process.exit(143);
|
|
186
189
|
});
|
|
187
190
|
this.config = await loadConfig().catch(() => ({}));
|
|
191
|
+
this.persona = this.config.persona ? { ...this.config.persona } : {};
|
|
188
192
|
for (const [name, value] of Object.entries(this.config.keys ?? {})) {
|
|
189
193
|
if (value && process.env[name] === undefined)
|
|
190
194
|
process.env[name] = value;
|
|
@@ -751,9 +755,15 @@ class TuiApp {
|
|
|
751
755
|
try {
|
|
752
756
|
const response = await provider.complete({
|
|
753
757
|
model: this.model,
|
|
754
|
-
system:
|
|
758
|
+
system: [
|
|
759
|
+
this.system,
|
|
760
|
+
personaSection(this.persona),
|
|
761
|
+
"# side channel\nThe user is asking a quick side question while the main task runs. Answer briefly and directly. Do not use tools, do not change the main task, and do not ask to run anything.",
|
|
762
|
+
]
|
|
763
|
+
.filter(Boolean)
|
|
764
|
+
.join("\n\n"),
|
|
755
765
|
messages: [
|
|
756
|
-
...this.session.getMessages(),
|
|
766
|
+
...sideChannelMessages(this.session.getMessages()),
|
|
757
767
|
{ role: "user", content: [{ type: "text", text }] },
|
|
758
768
|
],
|
|
759
769
|
maxTokens: 2048,
|
|
@@ -998,6 +1008,10 @@ class TuiApp {
|
|
|
998
1008
|
return;
|
|
999
1009
|
}
|
|
1000
1010
|
if (this.mode === "picker" && this.picker) {
|
|
1011
|
+
if (this.picker.backAt(row - this.lastHeaderRows)) {
|
|
1012
|
+
this.closePicker(null);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1001
1015
|
const value = this.picker.pickAt(row - this.lastHeaderRows);
|
|
1002
1016
|
if (value !== null)
|
|
1003
1017
|
this.closePicker(value);
|
|
@@ -1351,8 +1365,9 @@ class TuiApp {
|
|
|
1351
1365
|
return [rule, "", bottom];
|
|
1352
1366
|
}
|
|
1353
1367
|
statusLine() {
|
|
1354
|
-
const
|
|
1355
|
-
const
|
|
1368
|
+
const now = performance.now();
|
|
1369
|
+
const elapsedMs = now - this.statusStartedAt;
|
|
1370
|
+
const elapsed = (elapsedMs / 1000).toFixed(1);
|
|
1356
1371
|
const meta = [
|
|
1357
1372
|
this.status?.meta,
|
|
1358
1373
|
`${elapsed}s`,
|
|
@@ -1360,7 +1375,19 @@ class TuiApp {
|
|
|
1360
1375
|
]
|
|
1361
1376
|
.filter(Boolean)
|
|
1362
1377
|
.join(" · ");
|
|
1363
|
-
|
|
1378
|
+
const frames = glyphs.spinner;
|
|
1379
|
+
const glyph = frames[this.spinnerFrame % frames.length] ?? glyphs.diamond;
|
|
1380
|
+
const icon = fg(mix(palette.accent, palette.bone, pulse(now, 900)), glyph);
|
|
1381
|
+
const base = mix(palette.silver, palette.mist, 0.35);
|
|
1382
|
+
let label;
|
|
1383
|
+
if (this.status?.phase === "tool") {
|
|
1384
|
+
const name = this.status.label;
|
|
1385
|
+
label = `${tone.accent(toolGlyph(name))} ${shimmer(name, now / 55, { base, highlight: palette.bone })}`;
|
|
1386
|
+
}
|
|
1387
|
+
else {
|
|
1388
|
+
label = shimmer(this.status?.label ?? "", now / 55, { base, highlight: palette.bone });
|
|
1389
|
+
}
|
|
1390
|
+
return ` ${icon} ${label} ${tone.mist(meta)}`;
|
|
1364
1391
|
}
|
|
1365
1392
|
visibleTranscript(viewport) {
|
|
1366
1393
|
let contentEnd = this.transcript.length;
|
|
@@ -1423,7 +1450,7 @@ class TuiApp {
|
|
|
1423
1450
|
this.spinnerTimer = setInterval(() => {
|
|
1424
1451
|
this.spinnerFrame += 1;
|
|
1425
1452
|
this.scheduleRender();
|
|
1426
|
-
},
|
|
1453
|
+
}, 75);
|
|
1427
1454
|
if (typeof this.spinnerTimer.unref === "function")
|
|
1428
1455
|
this.spinnerTimer.unref();
|
|
1429
1456
|
}
|
|
@@ -1569,7 +1596,7 @@ class TuiApp {
|
|
|
1569
1596
|
this.agentMode = this.agentMode === "build" ? "plan" : "build";
|
|
1570
1597
|
this.rebuildTools();
|
|
1571
1598
|
this.append([
|
|
1572
|
-
` ${tone.accent(glyphs.diamond)} ${tone.mist(`// MODE → ${this.agentMode.toUpperCase()}`)}${this.agentMode === "plan" ? `
|
|
1599
|
+
` ${tone.accent(glyphs.diamond)} ${tone.mist(`// MODE → ${this.agentMode.toUpperCase()}`)}${this.agentMode === "plan" ? ` ${tone.amber("· no edits")}` : ""}`,
|
|
1573
1600
|
]);
|
|
1574
1601
|
}
|
|
1575
1602
|
ensureProvider() {
|
|
@@ -1955,21 +1982,50 @@ class TuiApp {
|
|
|
1955
1982
|
this.append([
|
|
1956
1983
|
` ${tone.amber(glyphs.diamondOpen)} ${tone.mist(`// ${req.tool} blocked in PLAN mode (Tab switches to BUILD)`)}`,
|
|
1957
1984
|
]);
|
|
1985
|
+
this.audit(req, "denied", "plan");
|
|
1958
1986
|
return { allowed: false };
|
|
1959
1987
|
}
|
|
1960
1988
|
if (this.agentMode === "plan" && READ_ONLY_TOOLS.has(req.tool))
|
|
1961
1989
|
return { allowed: true };
|
|
1962
|
-
|
|
1990
|
+
const analysis = req.tool === "run_command" ? this.commandAnalysis(req) : null;
|
|
1991
|
+
const dangerous = analysis?.dangerous === true;
|
|
1992
|
+
if (this.opts.allowAll) {
|
|
1993
|
+
this.audit(req, "allowed", "allow-all", analysis);
|
|
1963
1994
|
return { allowed: true };
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1995
|
+
}
|
|
1996
|
+
// "always" never covers dangerous commands — those are confirmed each time.
|
|
1997
|
+
if (this.gateMode === "always" && !dangerous) {
|
|
1998
|
+
this.audit(req, "allowed", "always", analysis);
|
|
1967
1999
|
return { allowed: true };
|
|
1968
2000
|
}
|
|
1969
|
-
|
|
2001
|
+
const choice = await this.permissionCard(req);
|
|
2002
|
+
const granted = choice === "allow" || choice === "always";
|
|
2003
|
+
if (choice === "always" && !dangerous)
|
|
2004
|
+
this.gateMode = "always";
|
|
2005
|
+
this.audit(req, granted ? "allowed" : "denied", choice, analysis);
|
|
2006
|
+
return { allowed: granted };
|
|
1970
2007
|
},
|
|
1971
2008
|
};
|
|
1972
2009
|
}
|
|
2010
|
+
commandAnalysis(req) {
|
|
2011
|
+
const command = req.input && typeof req.input === "object" && "command" in req.input
|
|
2012
|
+
? String(req.input.command)
|
|
2013
|
+
: "";
|
|
2014
|
+
return command ? analyzeCommand(command) : null;
|
|
2015
|
+
}
|
|
2016
|
+
audit(req, decision, via, analysis) {
|
|
2017
|
+
void appendAudit({
|
|
2018
|
+
ts: new Date().toISOString(),
|
|
2019
|
+
dir: this.dir,
|
|
2020
|
+
tool: req.tool,
|
|
2021
|
+
decision,
|
|
2022
|
+
via,
|
|
2023
|
+
danger: analysis
|
|
2024
|
+
? { dangerous: analysis.dangerous, sensitive: analysis.sensitive, reasons: analysis.reasons }
|
|
2025
|
+
: undefined,
|
|
2026
|
+
input: auditInput(req.input),
|
|
2027
|
+
}).catch(() => undefined);
|
|
2028
|
+
}
|
|
1973
2029
|
async promptForKey(keyEnv) {
|
|
1974
2030
|
this.append([` ${tone.mist(`// ${keyEnv} IS NOT SET`)}`]);
|
|
1975
2031
|
const result = await this.readLine({
|
|
@@ -2017,6 +2073,9 @@ class TuiApp {
|
|
|
2017
2073
|
const mcpSection = mcpInstructionsSection(this.mcp.instructions());
|
|
2018
2074
|
if (mcpSection)
|
|
2019
2075
|
system = system ? `${system}\n\n${mcpSection}` : mcpSection;
|
|
2076
|
+
const persona = personaSection(this.persona);
|
|
2077
|
+
if (persona)
|
|
2078
|
+
system = system ? `${system}\n\n${persona}` : persona;
|
|
2020
2079
|
if (this.agentMode === "plan") {
|
|
2021
2080
|
system = `${system ? `${system}\n\n` : ""}# mode\nYou are in PLAN mode: analyze the codebase and propose a plan. You may run read-only commands with run_command to inspect the project (each command is approved by the user), but never modify files, install packages or run mutating commands. When the plan is ready, present it and tell the user to press Tab to switch to BUILD mode.`;
|
|
2022
2081
|
}
|
|
@@ -2186,6 +2245,9 @@ class TuiApp {
|
|
|
2186
2245
|
` ${tone.mist(`// VERBOSE ${this.verbose ? "ON — FULL TOOL OUTPUT" : "OFF"}`)}`,
|
|
2187
2246
|
]);
|
|
2188
2247
|
return false;
|
|
2248
|
+
case "settings":
|
|
2249
|
+
await this.openSettings();
|
|
2250
|
+
return false;
|
|
2189
2251
|
case "theme":
|
|
2190
2252
|
await this.changeTheme(arg);
|
|
2191
2253
|
return false;
|
|
@@ -2206,6 +2268,45 @@ class TuiApp {
|
|
|
2206
2268
|
return false;
|
|
2207
2269
|
}
|
|
2208
2270
|
}
|
|
2271
|
+
async openSettings() {
|
|
2272
|
+
const before = JSON.stringify(this.persona);
|
|
2273
|
+
for (;;) {
|
|
2274
|
+
const entries = PERSONA_SETTINGS.map((setting) => ({
|
|
2275
|
+
kind: "item",
|
|
2276
|
+
value: setting.id,
|
|
2277
|
+
label: `${ASCII_MODE ? setting.asciiIcon : setting.icon} ${setting.label}`,
|
|
2278
|
+
hint: this.persona[setting.id] ?? AUTO,
|
|
2279
|
+
}));
|
|
2280
|
+
const id = await this.openPicker(new Picker({ title: "settings", entries, filterable: false }));
|
|
2281
|
+
if (!id)
|
|
2282
|
+
break;
|
|
2283
|
+
const setting = PERSONA_SETTINGS.find((entry) => entry.id === id);
|
|
2284
|
+
if (!setting)
|
|
2285
|
+
break;
|
|
2286
|
+
const current = this.persona[setting.id] ?? AUTO;
|
|
2287
|
+
const valueEntries = setting.options.map((option) => ({
|
|
2288
|
+
kind: "item",
|
|
2289
|
+
value: option.value,
|
|
2290
|
+
label: option.value === current ? `${glyphs.check} ${option.label}` : ` ${option.label}`,
|
|
2291
|
+
hint: option.description,
|
|
2292
|
+
}));
|
|
2293
|
+
const value = await this.openPicker(new Picker({ title: setting.label, entries: valueEntries, filterable: false }));
|
|
2294
|
+
if (value === null)
|
|
2295
|
+
continue;
|
|
2296
|
+
if (value === AUTO)
|
|
2297
|
+
delete this.persona[setting.id];
|
|
2298
|
+
else
|
|
2299
|
+
this.persona[setting.id] = value;
|
|
2300
|
+
}
|
|
2301
|
+
if (JSON.stringify(this.persona) !== before) {
|
|
2302
|
+
this.config.persona = this.persona;
|
|
2303
|
+
void saveConfig({ ...this.config }).catch(() => undefined);
|
|
2304
|
+
this.append([
|
|
2305
|
+
` ${tone.accent(glyphs.diamond)} ${tone.mist(`// SETTINGS → ${personaSummary(this.persona)}`)}`,
|
|
2306
|
+
"",
|
|
2307
|
+
]);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2209
2310
|
async changeTheme(arg) {
|
|
2210
2311
|
const current = activeThemeId();
|
|
2211
2312
|
if (arg) {
|
|
@@ -2947,6 +3048,7 @@ class TuiApp {
|
|
|
2947
3048
|
["mouse drag", "select text · ctrl+c copies"],
|
|
2948
3049
|
["click in input", "place the cursor"],
|
|
2949
3050
|
["permission card", "← → select · enter confirm · mouse click"],
|
|
3051
|
+
["pickers", "↑↓ move · enter select · ← or esc back · click rows and the ← back button"],
|
|
2950
3052
|
["sessions picker", "ctrl+r rename · ctrl+p pin · ctrl+d delete"],
|
|
2951
3053
|
["↑ ↓", "input history — previous / next message"],
|
|
2952
3054
|
["wheel", "scroll the conversation"],
|
|
@@ -3099,6 +3201,29 @@ const IMAGE_EXTENSIONS = {
|
|
|
3099
3201
|
".bmp": "image/bmp",
|
|
3100
3202
|
};
|
|
3101
3203
|
const IMAGE_MARKER_RE = /\[Image (\d+)\]/g;
|
|
3204
|
+
/**
|
|
3205
|
+
* A side-channel (/btw) request can run while a tool call is still in flight,
|
|
3206
|
+
* so the transcript may end with an assistant tool_call that has no tool result
|
|
3207
|
+
* yet. Providers reject such a sequence (400), so drop unanswered tool_calls.
|
|
3208
|
+
*/
|
|
3209
|
+
export function sideChannelMessages(messages) {
|
|
3210
|
+
const answered = new Set();
|
|
3211
|
+
for (const message of messages) {
|
|
3212
|
+
if (message.role === "tool")
|
|
3213
|
+
answered.add(message.toolCallId);
|
|
3214
|
+
}
|
|
3215
|
+
const out = [];
|
|
3216
|
+
for (const message of messages) {
|
|
3217
|
+
if (message.role !== "assistant" || !message.content.some((part) => part.type === "tool_call")) {
|
|
3218
|
+
out.push(message);
|
|
3219
|
+
continue;
|
|
3220
|
+
}
|
|
3221
|
+
const content = message.content.filter((part) => part.type !== "tool_call" || answered.has(part.id));
|
|
3222
|
+
if (content.length > 0)
|
|
3223
|
+
out.push({ ...message, content });
|
|
3224
|
+
}
|
|
3225
|
+
return out;
|
|
3226
|
+
}
|
|
3102
3227
|
export function nextImageIndex(text) {
|
|
3103
3228
|
const used = new Set();
|
|
3104
3229
|
for (const match of text.matchAll(IMAGE_MARKER_RE)) {
|