@quatcode/agent 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/args.js +1 -1
- package/dist/args.js.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/input.d.ts +3 -0
- package/dist/input.d.ts.map +1 -1
- package/dist/input.js +24 -2
- package/dist/input.js.map +1 -1
- package/dist/keyboard.d.ts +2 -0
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +14 -0
- package/dist/keyboard.js.map +1 -1
- package/dist/markdown.d.ts.map +1 -1
- package/dist/markdown.js +136 -2
- package/dist/markdown.js.map +1 -1
- package/dist/models.d.ts +11 -2
- package/dist/models.d.ts.map +1 -1
- package/dist/models.js +238 -12
- package/dist/models.js.map +1 -1
- package/dist/screen.d.ts +8 -1
- package/dist/screen.d.ts.map +1 -1
- package/dist/screen.js +27 -16
- package/dist/screen.js.map +1 -1
- package/dist/skills.js +1 -1
- package/dist/skills.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js.map +1 -1
- package/dist/theme.d.ts +5 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +10 -0
- package/dist/theme.js.map +1 -1
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +175 -36
- package/dist/tui.js.map +1 -1
- package/dist/turn.d.ts +12 -0
- package/dist/turn.d.ts.map +1 -1
- package/dist/turn.js +68 -24
- package/dist/turn.js.map +1 -1
- package/package.json +4 -4
package/dist/tui.js
CHANGED
|
@@ -8,11 +8,11 @@ import { Keyboard } from "./keyboard.js";
|
|
|
8
8
|
import { LineBuffer, defaultPrompt } from "./input.js";
|
|
9
9
|
import { Picker } from "./picker.js";
|
|
10
10
|
import { Screen } from "./screen.js";
|
|
11
|
-
import { TurnController, fmtCost, fmtDuration, fmtTokens } from "./turn.js";
|
|
11
|
+
import { TurnController, fmtCost, fmtDuration, fmtTokens, toolArg, toolResultBlock } from "./turn.js";
|
|
12
12
|
import { renderBanner } from "./logo.js";
|
|
13
13
|
import { renderMarkdown } from "./markdown.js";
|
|
14
|
-
import { ADD_PROVIDER, allProviders, buildProvider, defaultModelFor, effortsForModel, hasKey, keyEnvFor, probeProviderModels, providerEnvKey, 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, } from "./theme.js";
|
|
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";
|
|
16
16
|
import { listSessionsForDir, loadConfig, makeSessionId, saveConfig, saveSession, deleteSession } from "./store.js";
|
|
17
17
|
import { configFile, conversationKey, deleteConversation, displayTitle, latestPerConversation, messagesForStorage, sortConversations, updateConversation, } from "./store.js";
|
|
18
18
|
import { MCP_USAGE, isMcpUrl, mcpInstructionsSection, parseMcpAdd } from "./mcp.js";
|
|
@@ -50,6 +50,7 @@ const READ_ONLY_TOOLS = new Set([
|
|
|
50
50
|
"screenshot",
|
|
51
51
|
"list_servers",
|
|
52
52
|
]);
|
|
53
|
+
const PLAN_ALLOWED_TOOLS = new Set([...READ_ONLY_TOOLS, "run_command"]);
|
|
53
54
|
export async function runTui(options) {
|
|
54
55
|
const input = options.io?.input ?? process.stdin;
|
|
55
56
|
const output = options.io?.output ?? process.stdout;
|
|
@@ -82,6 +83,7 @@ class TuiApp {
|
|
|
82
83
|
gateMode;
|
|
83
84
|
verbose = false;
|
|
84
85
|
conversationId = makeSessionId();
|
|
86
|
+
providerSessionId = makeSessionId();
|
|
85
87
|
totalUsage = emptyUsage();
|
|
86
88
|
turns = 0;
|
|
87
89
|
transcript = [];
|
|
@@ -130,6 +132,9 @@ class TuiApp {
|
|
|
130
132
|
messageCursor = -1;
|
|
131
133
|
selection = null;
|
|
132
134
|
frameLines = [];
|
|
135
|
+
modeBadgeStart = -1;
|
|
136
|
+
modeBadgeWidth = 0;
|
|
137
|
+
modeBadgeHover = false;
|
|
133
138
|
constructor(options, input, output) {
|
|
134
139
|
this.opts = options;
|
|
135
140
|
this.dir = path.resolve(options.dir);
|
|
@@ -142,6 +147,11 @@ class TuiApp {
|
|
|
142
147
|
this.kb.start();
|
|
143
148
|
this.screen.enter();
|
|
144
149
|
this.kb.setHandler(this.onKeys);
|
|
150
|
+
this.kb.onSizeReport = (row, col) => {
|
|
151
|
+
if (this.screen.applySizeReport(row, col))
|
|
152
|
+
this.scheduleRender();
|
|
153
|
+
};
|
|
154
|
+
this.screen.requestSize();
|
|
145
155
|
const cleanup = () => {
|
|
146
156
|
try {
|
|
147
157
|
this.stopSpinner();
|
|
@@ -179,7 +189,6 @@ class TuiApp {
|
|
|
179
189
|
}
|
|
180
190
|
this.effort = this.opts.effort ?? this.config.effort ?? null;
|
|
181
191
|
setTheme(this.config.theme ?? "quatcode");
|
|
182
|
-
this.screen.applyBackground();
|
|
183
192
|
let configMeta = null;
|
|
184
193
|
try {
|
|
185
194
|
configMeta = await this.loadConfiguration();
|
|
@@ -320,9 +329,12 @@ class TuiApp {
|
|
|
320
329
|
onKeys = (keys) => {
|
|
321
330
|
for (const key of keys) {
|
|
322
331
|
if (key.type === "resize") {
|
|
323
|
-
this.screen.
|
|
332
|
+
this.screen.requestSize();
|
|
324
333
|
this.scheduleRender();
|
|
325
|
-
const settle = setTimeout(() =>
|
|
334
|
+
const settle = setTimeout(() => {
|
|
335
|
+
this.screen.requestSize();
|
|
336
|
+
this.scheduleRender();
|
|
337
|
+
}, 150);
|
|
326
338
|
if (typeof settle.unref === "function")
|
|
327
339
|
settle.unref();
|
|
328
340
|
continue;
|
|
@@ -683,7 +695,41 @@ class TuiApp {
|
|
|
683
695
|
}
|
|
684
696
|
return lines;
|
|
685
697
|
}
|
|
698
|
+
badgeHit(col) {
|
|
699
|
+
if (this.modeBadgeStart < 0 || this.modeBadgeWidth <= 0)
|
|
700
|
+
return false;
|
|
701
|
+
const start = this.padX + this.modeBadgeStart;
|
|
702
|
+
return col >= start && col < start + this.modeBadgeWidth;
|
|
703
|
+
}
|
|
704
|
+
placeInputCursor(row, col) {
|
|
705
|
+
const rendered = this.lastInputRender;
|
|
706
|
+
if (!rendered)
|
|
707
|
+
return false;
|
|
708
|
+
const top = this.lastHeaderRows + this.lastViewport + this.lastInputOffset;
|
|
709
|
+
const lineIndex = row - top;
|
|
710
|
+
if (lineIndex < 0 || lineIndex >= rendered.lines.length)
|
|
711
|
+
return false;
|
|
712
|
+
const prefixWidth = rendered.prefixWidths[lineIndex] ?? 0;
|
|
713
|
+
const textCol = col - this.padX - prefixWidth;
|
|
714
|
+
const start = rendered.lineStarts[lineIndex] ?? 0;
|
|
715
|
+
this.buffer.setCursorFromDisplay(lineIndex, Math.max(0, textCol), start);
|
|
716
|
+
this.scheduleRender();
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
686
719
|
handleHover(row, col) {
|
|
720
|
+
if (row === 1) {
|
|
721
|
+
const hover = this.badgeHit(col);
|
|
722
|
+
if (hover !== this.modeBadgeHover) {
|
|
723
|
+
this.modeBadgeHover = hover;
|
|
724
|
+
this.scheduleRender();
|
|
725
|
+
}
|
|
726
|
+
if (hover)
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
else if (this.modeBadgeHover) {
|
|
730
|
+
this.modeBadgeHover = false;
|
|
731
|
+
this.scheduleRender();
|
|
732
|
+
}
|
|
687
733
|
if (this.mode === "permission") {
|
|
688
734
|
const index = this.permissionOptionAt(row, col);
|
|
689
735
|
if (index !== null && index !== this.permissionSelected) {
|
|
@@ -710,6 +756,12 @@ class TuiApp {
|
|
|
710
756
|
}
|
|
711
757
|
}
|
|
712
758
|
handleClick(row, col) {
|
|
759
|
+
if (row === 1 &&
|
|
760
|
+
(this.mode === "editing" || this.mode === "idle") &&
|
|
761
|
+
this.badgeHit(col)) {
|
|
762
|
+
this.toggleMode();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
713
765
|
if (this.mode === "permission") {
|
|
714
766
|
const index = this.permissionOptionAt(row, col);
|
|
715
767
|
if (index !== null) {
|
|
@@ -722,6 +774,8 @@ class TuiApp {
|
|
|
722
774
|
return;
|
|
723
775
|
}
|
|
724
776
|
if (this.mode === "editing") {
|
|
777
|
+
if (this.placeInputCursor(row, col))
|
|
778
|
+
return;
|
|
725
779
|
const matches = this.suggestionMatches();
|
|
726
780
|
if (matches.length > 0) {
|
|
727
781
|
const top = this.lastHeaderRows + this.lastViewport + 1;
|
|
@@ -999,28 +1053,33 @@ class TuiApp {
|
|
|
999
1053
|
const modelLabel = this.model
|
|
1000
1054
|
? `${this.providerId} · ${this.model}${effort ? ` · ${effort}` : ""}`
|
|
1001
1055
|
: "";
|
|
1002
|
-
const
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1056
|
+
const badgeText = this.agentMode === "plan" ? " PLAN " : " BUILD ";
|
|
1057
|
+
const badge = this.modeBadgeHover
|
|
1058
|
+
? fg(palette.bone, bg(palette.steel, badgeText))
|
|
1059
|
+
: this.agentMode === "plan"
|
|
1060
|
+
? fg(palette.amber, bg(palette.graphite, badgeText))
|
|
1061
|
+
: tone.onGraphite(badgeText);
|
|
1062
|
+
const before = [
|
|
1006
1063
|
bold(tone.bone("quatcode")),
|
|
1007
1064
|
modelLabel ? tone.steel("·") : "",
|
|
1008
1065
|
modelLabel ? tone.silver(modelLabel) : "",
|
|
1009
|
-
badge,
|
|
1010
1066
|
]
|
|
1011
1067
|
.filter(Boolean)
|
|
1012
1068
|
.join(" ");
|
|
1069
|
+
this.modeBadgeStart = 2 + stringWidth(before) + 2;
|
|
1070
|
+
this.modeBadgeWidth = stringWidth(badge);
|
|
1071
|
+
const left = `${before} ${badge}`;
|
|
1013
1072
|
const scrolled = this.scrollOffset > 0 ? tone.mist("↑ ") : "";
|
|
1014
1073
|
const right = `${scrolled}${tone.mist(`v${VERSION}`)}`;
|
|
1015
1074
|
const gap = Math.max(1, width - 2 - stringWidth(left) - stringWidth(right));
|
|
1016
1075
|
return [
|
|
1017
1076
|
` ${left}${" ".repeat(gap)}${right}`,
|
|
1018
|
-
` ${tone.
|
|
1077
|
+
` ${tone.mist("─".repeat(Math.max(8, width - 2)))}`,
|
|
1019
1078
|
];
|
|
1020
1079
|
}
|
|
1021
1080
|
footerLines() {
|
|
1022
1081
|
const width = this.contentWidth();
|
|
1023
|
-
const
|
|
1082
|
+
const rule = ` ${tone.mist("─".repeat(Math.max(8, width - 2)))}`;
|
|
1024
1083
|
if (this.mode === "editing") {
|
|
1025
1084
|
const rendered = this.buffer.render({
|
|
1026
1085
|
width,
|
|
@@ -1032,20 +1091,20 @@ class TuiApp {
|
|
|
1032
1091
|
this.lastInputRender = rendered;
|
|
1033
1092
|
const suggestions = this.suggestions();
|
|
1034
1093
|
this.lastInputOffset = 1 + suggestions.length;
|
|
1035
|
-
return [
|
|
1094
|
+
return [rule, ...suggestions, ...rendered.lines, rule];
|
|
1036
1095
|
}
|
|
1037
1096
|
this.lastInputRender = null;
|
|
1038
1097
|
if (this.status)
|
|
1039
|
-
return [
|
|
1098
|
+
return [rule, this.statusLine(), rule];
|
|
1040
1099
|
if (this.mode === "running") {
|
|
1041
|
-
return [
|
|
1100
|
+
return [rule, ` ${tone.mist("// esc interrupts")}`, rule];
|
|
1042
1101
|
}
|
|
1043
1102
|
if (this.mode === "permission")
|
|
1044
|
-
return [
|
|
1103
|
+
return [rule, ` ${this.permissionHint}`, rule];
|
|
1045
1104
|
if (this.mode === "picker" && this.picker) {
|
|
1046
|
-
return [
|
|
1105
|
+
return [rule, this.picker.footer(this.picker.itemCount()), rule];
|
|
1047
1106
|
}
|
|
1048
|
-
return [
|
|
1107
|
+
return [rule, ` ${tone.mist(`// ${shortenHome(this.dir)}`)}`, rule];
|
|
1049
1108
|
}
|
|
1050
1109
|
statusLine() {
|
|
1051
1110
|
const spin = tone.accent(glyphs.spinner[this.spinnerFrame % glyphs.spinner.length] ?? "-");
|
|
@@ -1225,7 +1284,7 @@ class TuiApp {
|
|
|
1225
1284
|
const readOnlyMcp = this.mcp.readOnlyToolNames();
|
|
1226
1285
|
const filtered = new ToolRegistry();
|
|
1227
1286
|
for (const tool of full.list()) {
|
|
1228
|
-
if (
|
|
1287
|
+
if (PLAN_ALLOWED_TOOLS.has(tool.name) || readOnlyMcp.has(tool.name))
|
|
1229
1288
|
filtered.register(tool);
|
|
1230
1289
|
}
|
|
1231
1290
|
filtered.onDispose(() => full.dispose());
|
|
@@ -1258,14 +1317,16 @@ class TuiApp {
|
|
|
1258
1317
|
this.agentMode = this.agentMode === "build" ? "plan" : "build";
|
|
1259
1318
|
this.rebuildTools();
|
|
1260
1319
|
this.append([
|
|
1261
|
-
` ${tone.accent(glyphs.diamond)} ${tone.mist(`// MODE → ${this.agentMode.toUpperCase()}`)}${this.agentMode === "plan" ? ` ${tone.amber("
|
|
1320
|
+
` ${tone.accent(glyphs.diamond)} ${tone.mist(`// MODE → ${this.agentMode.toUpperCase()}`)}${this.agentMode === "plan" ? ` ${tone.amber("no edits · run_command allowed (with approval)")}` : ""}`,
|
|
1262
1321
|
]);
|
|
1263
1322
|
}
|
|
1264
1323
|
ensureProvider() {
|
|
1265
1324
|
if (this.provider)
|
|
1266
1325
|
return this.provider;
|
|
1267
1326
|
try {
|
|
1268
|
-
this.provider = buildProvider(this.providerId, this.model
|
|
1327
|
+
this.provider = buildProvider(this.providerId, this.model, {
|
|
1328
|
+
sessionId: this.providerSessionId,
|
|
1329
|
+
});
|
|
1269
1330
|
return this.provider;
|
|
1270
1331
|
}
|
|
1271
1332
|
catch {
|
|
@@ -1399,14 +1460,14 @@ class TuiApp {
|
|
|
1399
1460
|
}
|
|
1400
1461
|
const urlResult = await this.readLine({
|
|
1401
1462
|
prompt: ` ${tone.bone("BASE URL")} ${tone.mist(glyphs.prompt)} `,
|
|
1402
|
-
placeholder: "https://api.example.com/v1",
|
|
1463
|
+
placeholder: "https://api.example.com/v1 — the API base, not the website",
|
|
1403
1464
|
});
|
|
1404
1465
|
if (urlResult.kind !== "submit" || !urlResult.text.trim()) {
|
|
1405
1466
|
this.append([` ${tone.mist("// cancelled")}`]);
|
|
1406
1467
|
return false;
|
|
1407
1468
|
}
|
|
1408
|
-
const
|
|
1409
|
-
if (!/^https?:\/\//i.test(
|
|
1469
|
+
const rawBase = urlResult.text.trim().replace(/\/+$/, "");
|
|
1470
|
+
if (!/^https?:\/\//i.test(rawBase)) {
|
|
1410
1471
|
this.append([` ${tone.red("// base url must start with http:// or https://")}`]);
|
|
1411
1472
|
return false;
|
|
1412
1473
|
}
|
|
@@ -1422,14 +1483,17 @@ class TuiApp {
|
|
|
1422
1483
|
const apiKey = keyResult.text.trim();
|
|
1423
1484
|
this.setStatus({ label: `CHECKING ${id.toUpperCase()}` });
|
|
1424
1485
|
let models = [];
|
|
1486
|
+
let baseUrl = rawBase;
|
|
1425
1487
|
try {
|
|
1426
|
-
|
|
1488
|
+
const probe = await probeProviderModels(rawBase, apiKey);
|
|
1489
|
+
baseUrl = probe.baseUrl;
|
|
1490
|
+
models = probe.models;
|
|
1427
1491
|
}
|
|
1428
1492
|
catch (err) {
|
|
1429
1493
|
this.setStatus(null);
|
|
1430
1494
|
this.append([
|
|
1431
1495
|
` ${tone.red(`// ${errorMessage(err)}`)}`,
|
|
1432
|
-
` ${tone.mist("// the provider must expose an OpenAI-compatible GET /models")}`,
|
|
1496
|
+
` ${tone.mist("// the provider must expose an OpenAI-compatible GET /models (usually <base>/v1)")}`,
|
|
1433
1497
|
]);
|
|
1434
1498
|
return false;
|
|
1435
1499
|
}
|
|
@@ -1635,12 +1699,14 @@ class TuiApp {
|
|
|
1635
1699
|
buildGate() {
|
|
1636
1700
|
return {
|
|
1637
1701
|
request: async (req) => {
|
|
1638
|
-
if (this.agentMode === "plan" && !
|
|
1702
|
+
if (this.agentMode === "plan" && !PLAN_ALLOWED_TOOLS.has(req.tool)) {
|
|
1639
1703
|
this.append([
|
|
1640
1704
|
` ${tone.amber(glyphs.diamondOpen)} ${tone.mist(`// ${req.tool} blocked in PLAN mode (Tab switches to BUILD)`)}`,
|
|
1641
1705
|
]);
|
|
1642
1706
|
return { allowed: false };
|
|
1643
1707
|
}
|
|
1708
|
+
if (this.agentMode === "plan" && READ_ONLY_TOOLS.has(req.tool))
|
|
1709
|
+
return { allowed: true };
|
|
1644
1710
|
if (this.gateMode === "always")
|
|
1645
1711
|
return { allowed: true };
|
|
1646
1712
|
const choice = await this.permissionCard(req);
|
|
@@ -1700,7 +1766,7 @@ class TuiApp {
|
|
|
1700
1766
|
if (mcpSection)
|
|
1701
1767
|
system = system ? `${system}\n\n${mcpSection}` : mcpSection;
|
|
1702
1768
|
if (this.agentMode === "plan") {
|
|
1703
|
-
system = `${system ? `${system}\n\n` : ""}# mode\nYou are in PLAN mode: analyze the codebase and propose a plan.
|
|
1769
|
+
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.`;
|
|
1704
1770
|
}
|
|
1705
1771
|
this.abort = new AbortController();
|
|
1706
1772
|
this.abortRequested = false;
|
|
@@ -1777,6 +1843,11 @@ class TuiApp {
|
|
|
1777
1843
|
? "max steps"
|
|
1778
1844
|
: "failed";
|
|
1779
1845
|
controller.footer(status);
|
|
1846
|
+
if (result.status === "max_steps" && !this.abortRequested) {
|
|
1847
|
+
this.append([
|
|
1848
|
+
` ${tone.amber(glyphs.diamondOpen)} ${tone.mist(`// STEP LIMIT REACHED (${this.opts.maxSteps}) — send any message to continue where it left off, or start with --max-steps <n>`)}`,
|
|
1849
|
+
]);
|
|
1850
|
+
}
|
|
1780
1851
|
const recordId = await this.saveTurn(input, result, controller.durationMs);
|
|
1781
1852
|
if (this.turnUndo) {
|
|
1782
1853
|
this.lastTurn = { ...this.turnUndo, recordId };
|
|
@@ -1796,6 +1867,7 @@ class TuiApp {
|
|
|
1796
1867
|
case "new":
|
|
1797
1868
|
this.session = new Session(this.system);
|
|
1798
1869
|
this.conversationId = makeSessionId();
|
|
1870
|
+
this.providerSessionId = makeSessionId();
|
|
1799
1871
|
this.lastTurn = null;
|
|
1800
1872
|
this.pendingImages = new Map();
|
|
1801
1873
|
this.transcript = [];
|
|
@@ -1901,7 +1973,6 @@ class TuiApp {
|
|
|
1901
1973
|
this.transcript = this.transcript.map((line) => remapColors(line, pairs));
|
|
1902
1974
|
this.config.theme = theme.id;
|
|
1903
1975
|
void saveConfig({ ...this.config, theme: theme.id }).catch(() => undefined);
|
|
1904
|
-
this.screen.applyBackground();
|
|
1905
1976
|
this.append([
|
|
1906
1977
|
` ${tone.accent(glyphs.diamond)} ${tone.mist(`// THEME → ${theme.label.toUpperCase()}`)}`,
|
|
1907
1978
|
"",
|
|
@@ -1922,7 +1993,7 @@ class TuiApp {
|
|
|
1922
1993
|
kind: "item",
|
|
1923
1994
|
value: conversationKey(record),
|
|
1924
1995
|
label: `${record.pinned ? `${glyphs.pin} ` : ""}${clipAnsi(displayTitle(record), labelWidth)}`,
|
|
1925
|
-
hint: `${record.pinned ? "pinned · " : ""}${record.createdAt.slice(0, 16).replace("T", " ")} · ${record.model} · ${record.status} · ${record.steps} steps`,
|
|
1996
|
+
hint: `${record.pinned ? "pinned · " : ""}${record.createdAt.slice(0, 16).replace("T", " ")} · ${record.provider ? `${record.provider} · ` : ""}${record.model} · ${record.status} · ${record.steps} steps`,
|
|
1926
1997
|
}));
|
|
1927
1998
|
const chosen = await this.openPicker(new Picker({
|
|
1928
1999
|
title: "sessions · enter resumes",
|
|
@@ -1986,25 +2057,91 @@ class TuiApp {
|
|
|
1986
2057
|
const messages = record.messages ?? [];
|
|
1987
2058
|
this.session = new Session(this.system, messages);
|
|
1988
2059
|
this.conversationId = conversationKey(record);
|
|
2060
|
+
this.providerSessionId = makeSessionId();
|
|
1989
2061
|
if (record.model) {
|
|
1990
|
-
|
|
2062
|
+
const resolved = record.provider && providerHasModel(record.provider, record.model)
|
|
2063
|
+
? record.provider
|
|
2064
|
+
: providerHasModel(this.providerId, record.model)
|
|
2065
|
+
? this.providerId
|
|
2066
|
+
: resolveProviderForModel(record.model);
|
|
2067
|
+
if (resolved)
|
|
2068
|
+
this.providerId = resolved;
|
|
1991
2069
|
this.model = record.model;
|
|
1992
2070
|
this.provider = null;
|
|
1993
2071
|
}
|
|
1994
2072
|
const task = record.task.replace(/\s+/g, " ");
|
|
1995
2073
|
this.append([
|
|
1996
2074
|
` ${tone.accent(glyphs.diamond)} ${tone.mist(`// RESUMED → ${clipAnsi(task, this.contentWidth() - 20)}`)}`,
|
|
1997
|
-
` ${tone.silver(`${messages.length} message${messages.length === 1 ? "" : "s"} · ${record.model} · ${record.createdAt.slice(0, 16).replace("T", " ")}`)}`,
|
|
2075
|
+
` ${tone.silver(`${messages.length} message${messages.length === 1 ? "" : "s"} · ${record.provider ? `${record.provider} · ` : ""}${record.model} · ${record.createdAt.slice(0, 16).replace("T", " ")}`)}`,
|
|
1998
2076
|
]);
|
|
1999
|
-
if (
|
|
2077
|
+
if (messages.length > 0) {
|
|
2078
|
+
this.append(this.replaySession(messages));
|
|
2079
|
+
}
|
|
2080
|
+
else if (record.text) {
|
|
2000
2081
|
this.append([` ${tone.mist("// LAST ANSWER")}`]);
|
|
2001
2082
|
this.append(renderMarkdown(record.text, { width: this.contentWidth() - 2 }));
|
|
2002
2083
|
}
|
|
2003
|
-
|
|
2084
|
+
else {
|
|
2004
2085
|
this.append([` ${tone.mist("// no stored messages for this session (saved before resume support)")}`]);
|
|
2005
2086
|
}
|
|
2006
2087
|
this.append([""]);
|
|
2007
2088
|
}
|
|
2089
|
+
replaySession(messages) {
|
|
2090
|
+
const width = Math.max(40, this.contentWidth() - 2);
|
|
2091
|
+
const calls = new Map();
|
|
2092
|
+
const results = new Set();
|
|
2093
|
+
for (const message of messages) {
|
|
2094
|
+
if (message.role === "assistant") {
|
|
2095
|
+
for (const part of message.content) {
|
|
2096
|
+
if (part.type === "tool_call")
|
|
2097
|
+
calls.set(part.id, { name: part.name, input: part.input });
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
else if (message.role === "tool") {
|
|
2101
|
+
results.add(message.toolCallId);
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
const lines = [];
|
|
2105
|
+
for (const message of messages) {
|
|
2106
|
+
if (message.role === "user") {
|
|
2107
|
+
const text = message.content
|
|
2108
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
2109
|
+
.join("\n");
|
|
2110
|
+
const images = message.content.filter((part) => part.type === "image").length;
|
|
2111
|
+
if (text.trim() || images > 0)
|
|
2112
|
+
lines.push(...this.userBlockLines(text, images));
|
|
2113
|
+
continue;
|
|
2114
|
+
}
|
|
2115
|
+
if (message.role === "assistant") {
|
|
2116
|
+
const text = message.content
|
|
2117
|
+
.flatMap((part) => (part.type === "text" ? [part.text] : []))
|
|
2118
|
+
.join("");
|
|
2119
|
+
if (text.trim()) {
|
|
2120
|
+
lines.push(` ${tone.mist("// AGENT")}`);
|
|
2121
|
+
lines.push(...renderMarkdown(text, { width }));
|
|
2122
|
+
}
|
|
2123
|
+
for (const part of message.content) {
|
|
2124
|
+
if (part.type !== "tool_call" || results.has(part.id))
|
|
2125
|
+
continue;
|
|
2126
|
+
const arg = toolArg(part.input, Math.max(20, width - 28));
|
|
2127
|
+
lines.push("", clipAnsi(` ${tone.mist(toolGlyph(part.name))} ${bold(part.name)}${arg ? ` ${tone.silver(arg)}` : ""} ${tone.mist("// no result stored")}`, width - 1));
|
|
2128
|
+
}
|
|
2129
|
+
continue;
|
|
2130
|
+
}
|
|
2131
|
+
const call = calls.get(message.toolCallId);
|
|
2132
|
+
const name = call?.name ?? "tool";
|
|
2133
|
+
const arg = call ? toolArg(call.input, Math.max(20, width - 28)) : "";
|
|
2134
|
+
lines.push(...toolResultBlock({
|
|
2135
|
+
tool: name,
|
|
2136
|
+
arg,
|
|
2137
|
+
content: message.content,
|
|
2138
|
+
isError: Boolean(message.isError),
|
|
2139
|
+
width,
|
|
2140
|
+
verbose: this.verbose,
|
|
2141
|
+
}));
|
|
2142
|
+
}
|
|
2143
|
+
return lines;
|
|
2144
|
+
}
|
|
2008
2145
|
async skillsCommand(arg) {
|
|
2009
2146
|
const parts = arg.split(/\s+/).filter(Boolean);
|
|
2010
2147
|
const sub = (parts[0] ?? "").toLowerCase();
|
|
@@ -2509,12 +2646,13 @@ class TuiApp {
|
|
|
2509
2646
|
["ctrl+enter", "newline (shift+enter, ctrl+space, ctrl+j, \\ + enter too)"],
|
|
2510
2647
|
["ctrl+v / alt+v", "paste text or an image — alt+v works even if the terminal eats ctrl+v"],
|
|
2511
2648
|
["mouse drag", "select text · ctrl+c copies"],
|
|
2649
|
+
["click in input", "place the cursor"],
|
|
2512
2650
|
["permission card", "← → select · enter confirm · mouse click"],
|
|
2513
2651
|
["sessions picker", "ctrl+r rename · ctrl+p pin · ctrl+d delete"],
|
|
2514
2652
|
["↑ ↓", "input history — previous / next message"],
|
|
2515
2653
|
["wheel", "scroll the conversation"],
|
|
2516
2654
|
["ctrl+p/ctrl+n", "input history (same as ↑ ↓)"],
|
|
2517
|
-
["tab", "plan/build
|
|
2655
|
+
["tab / badge", "switch plan/build · click the header badge · complete commands"],
|
|
2518
2656
|
["ctrl+z", "undo the last message (works while thinking)"],
|
|
2519
2657
|
["esc", "interrupt a run"],
|
|
2520
2658
|
["ctrl+c", "clear input / exit"],
|
|
@@ -2546,6 +2684,7 @@ class TuiApp {
|
|
|
2546
2684
|
createdAt: new Date().toISOString(),
|
|
2547
2685
|
cwd: this.dir,
|
|
2548
2686
|
task: input,
|
|
2687
|
+
provider: this.providerId,
|
|
2549
2688
|
model: this.model,
|
|
2550
2689
|
status: result.status,
|
|
2551
2690
|
steps: result.steps,
|