@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/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.applyBackground();
332
+ this.screen.requestSize();
324
333
  this.scheduleRender();
325
- const settle = setTimeout(() => this.scheduleRender(), 150);
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 badge = this.agentMode === "plan"
1003
- ? fg(palette.amber, bg(palette.graphite, " PLAN "))
1004
- : tone.onGraphite(" BUILD ");
1005
- const left = [
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.steel("─".repeat(Math.max(8, width - 2)))}`,
1077
+ ` ${tone.mist("─".repeat(Math.max(8, width - 2)))}`,
1019
1078
  ];
1020
1079
  }
1021
1080
  footerLines() {
1022
1081
  const width = this.contentWidth();
1023
- const bar = ` ${tone.steel("─".repeat(Math.max(8, width - 2)))}`;
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 [bar, ...suggestions, ...rendered.lines];
1094
+ return [rule, ...suggestions, ...rendered.lines, rule];
1036
1095
  }
1037
1096
  this.lastInputRender = null;
1038
1097
  if (this.status)
1039
- return [bar, this.statusLine()];
1098
+ return [rule, this.statusLine(), rule];
1040
1099
  if (this.mode === "running") {
1041
- return [bar, ` ${tone.mist("// esc interrupts")}`];
1100
+ return [rule, ` ${tone.mist("// esc interrupts")}`, rule];
1042
1101
  }
1043
1102
  if (this.mode === "permission")
1044
- return [bar, ` ${this.permissionHint}`];
1103
+ return [rule, ` ${this.permissionHint}`, rule];
1045
1104
  if (this.mode === "picker" && this.picker) {
1046
- return [bar, this.picker.footer(this.picker.itemCount())];
1105
+ return [rule, this.picker.footer(this.picker.itemCount()), rule];
1047
1106
  }
1048
- return [bar, ` ${tone.mist(`// ${shortenHome(this.dir)}`)}`];
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 (READ_ONLY_TOOLS.has(tool.name) || readOnlyMcp.has(tool.name))
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("read-only: no edits, no commands")}` : ""}`,
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 baseUrl = urlResult.text.trim().replace(/\/+$/, "");
1409
- if (!/^https?:\/\//i.test(baseUrl)) {
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
- models = await probeProviderModels(baseUrl, apiKey);
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" && !READ_ONLY_TOOLS.has(req.tool)) {
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. Do not modify files or run commands. When the plan is ready, present it and tell the user to press Tab to switch to BUILD mode.`;
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
- this.providerId = resolveProviderForModel(record.model) ?? this.providerId;
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 (record.text) {
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
- if (messages.length === 0) {
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 mode · complete commands"],
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,