u-foo 2.5.7 → 2.5.9

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.
@@ -12,6 +12,10 @@ const {
12
12
  resolveClaudeOauthPaths,
13
13
  resolveClaudeUpstreamCredentials,
14
14
  } = require("./credentials/claude");
15
+ const {
16
+ resolveKimiCredentialPaths,
17
+ resolveKimiUpstreamCredentials,
18
+ } = require("./credentials/kimi");
15
19
 
16
20
  function normalizeRefreshWindowMs(value) {
17
21
  const num = Number(value);
@@ -27,6 +31,7 @@ function normalizeDirectAuthProvider(value = "") {
27
31
  const text = String(value || "").trim().toLowerCase();
28
32
  if (text === "claude" || text === "claude-cli" || text === "claude-code" || text === "anthropic") return "claude";
29
33
  if (text === "agy" || text === "agy-cli" || text === "antigravity") return "agy";
34
+ if (text === "kimi" || text === "kimi-code" || text === "moonshot") return "kimi";
30
35
  return "codex";
31
36
  }
32
37
 
@@ -272,6 +277,41 @@ async function inspectAgyDirectAuth({
272
277
  };
273
278
  }
274
279
 
280
+ async function inspectKimiDirectAuth({
281
+ env = process.env,
282
+ fetchImpl = global.fetch,
283
+ autoRefresh = false,
284
+ } = {}) {
285
+ const paths = resolveKimiCredentialPaths({ env });
286
+ try {
287
+ const credential = await resolveKimiUpstreamCredentials({
288
+ env,
289
+ fetchImpl,
290
+ autoRefresh,
291
+ });
292
+ return {
293
+ ok: true,
294
+ provider: "kimi",
295
+ transport: "openai-chat",
296
+ credentialKind: String(credential.credentialKind || ""),
297
+ source: String(credential.source || ""),
298
+ state: String(credential.state || ""),
299
+ refreshable: credential.refreshable === true,
300
+ expiresAt: String(credential.expiresAt || ""),
301
+ credentialPath: String(credential.credentialPath || paths.credentialPath || ""),
302
+ };
303
+ } catch (err) {
304
+ return {
305
+ ok: false,
306
+ provider: "kimi",
307
+ error: err && err.message ? err.message : "Kimi direct API credentials are unavailable",
308
+ errorCode: normalizeErrorCode(err, "KIMI_AUTH_STATUS_FAILED"),
309
+ credentialPath: paths.credentialPath || "",
310
+ hint: "Run `kimi` once to sign in or set UFOO_UCODE_API_KEY; ufoo-agent will not fall back to the CLI.",
311
+ };
312
+ }
313
+ }
314
+
275
315
  async function inspectDirectAuthStatus(options = {}) {
276
316
  const { projectRoot, loadConfigImpl = loadConfig, provider = "" } = options;
277
317
  const config = loadConfigImpl(projectRoot) || {};
@@ -286,6 +326,9 @@ async function inspectDirectAuthStatus(options = {}) {
286
326
  if (selected === "agy") {
287
327
  return inspectAgyDirectAuth(nextOptions);
288
328
  }
329
+ if (selected === "kimi") {
330
+ return inspectKimiDirectAuth(nextOptions);
331
+ }
289
332
  return inspectCodexDirectAuth(nextOptions);
290
333
  }
291
334
 
@@ -423,6 +466,52 @@ function formatAgyDirectAuthStatus(status = {}, options = {}) {
423
466
  return lines;
424
467
  }
425
468
 
469
+ function formatKimiDirectAuthStatus(status = {}, options = {}) {
470
+ if (options.compact === true) {
471
+ if (status.ok) {
472
+ const credential = status.credentialKind || "credential";
473
+ const transport = status.transport || "openai-chat";
474
+ const state = status.state || "unknown";
475
+ const details = [
476
+ status.source || "",
477
+ status.expiresAt ? `expires ${formatCompactExpires(status.expiresAt)}` : "",
478
+ status.refreshable ? "refreshable" : "",
479
+ ].filter(Boolean);
480
+ const lines = [
481
+ `Kimi API: OK · ${credential}/${transport} · ${state}`,
482
+ ];
483
+ if (details.length > 0) lines.push(` ${details.join(" · ")}`);
484
+ return lines;
485
+ }
486
+
487
+ const hint = String(status.hint || "Run `kimi` once to sign in or set UFOO_UCODE_API_KEY.").replace(/;.*$/, ".");
488
+ return [
489
+ `Kimi API: FAIL · ${status.errorCode || "KIMI_AUTH_STATUS_FAILED"}`,
490
+ ` ${status.error || "Kimi direct API credentials are unavailable"} · ${hint}`,
491
+ ];
492
+ }
493
+
494
+ if (status.ok) {
495
+ const state = status.state || "unknown";
496
+ const lines = [
497
+ `Kimi direct API: OK (${status.transport || "openai-chat"}, ${status.credentialKind || "credential"}, ${state})`,
498
+ ` - source: ${status.source || "(unknown)"}`,
499
+ ];
500
+ if (status.expiresAt) lines.push(` - expires: ${status.expiresAt}`);
501
+ if (status.credentialPath) lines.push(` - path: ${status.credentialPath}`);
502
+ if (status.refreshable) lines.push(" - refreshable: yes");
503
+ return lines;
504
+ }
505
+
506
+ const lines = [
507
+ `Kimi direct API: FAIL (${status.errorCode || "KIMI_AUTH_STATUS_FAILED"})`,
508
+ ` - ${status.error || "Kimi direct API credentials are unavailable"}`,
509
+ ];
510
+ if (status.credentialPath) lines.push(` - expected path: ${status.credentialPath}`);
511
+ if (status.hint) lines.push(` - ${status.hint}`);
512
+ return lines;
513
+ }
514
+
426
515
  function formatDirectAuthStatus(status = {}, options = {}) {
427
516
  if (status.provider === "claude") {
428
517
  return formatClaudeDirectAuthStatus(status, options);
@@ -430,6 +519,9 @@ function formatDirectAuthStatus(status = {}, options = {}) {
430
519
  if (status.provider === "agy") {
431
520
  return formatAgyDirectAuthStatus(status, options);
432
521
  }
522
+ if (status.provider === "kimi") {
523
+ return formatKimiDirectAuthStatus(status, options);
524
+ }
433
525
  return formatCodexDirectAuthStatus(status, options);
434
526
  }
435
527
 
@@ -438,10 +530,12 @@ module.exports = {
438
530
  inspectCodexDirectAuth,
439
531
  inspectClaudeDirectAuth,
440
532
  inspectAgyDirectAuth,
533
+ inspectKimiDirectAuth,
441
534
  formatDirectAuthStatus,
442
535
  formatCodexDirectAuthStatus,
443
536
  formatClaudeDirectAuthStatus,
444
537
  formatAgyDirectAuthStatus,
538
+ formatKimiDirectAuthStatus,
445
539
  normalizeDirectAuthProvider,
446
540
  classifyAgyLogTail,
447
541
  };
@@ -59,6 +59,9 @@ function normalizeSettingsProvider(value = "", fallback = "codex-cli") {
59
59
  if (text === "agy" || text === "agy-cli" || text === "antigravity") {
60
60
  return "agy-cli";
61
61
  }
62
+ if (text === "kimi" || text === "kimi-cli" || text === "kimi-code" || text === "ukimi") {
63
+ return "kimi-cli";
64
+ }
62
65
  if (text === "codex" || text === "codex-cli" || text === "codex-code" || text === "openai") {
63
66
  return "codex-cli";
64
67
  }
@@ -69,6 +72,7 @@ function agentProviderKey(value = "") {
69
72
  const provider = normalizeSettingsProvider(value);
70
73
  if (provider === "claude-cli") return "claude";
71
74
  if (provider === "agy-cli") return "agy";
75
+ if (provider === "kimi-cli") return "kimi";
72
76
  return "codex";
73
77
  }
74
78
 
@@ -597,7 +601,7 @@ function createCommandExecutor(options = {}) {
597
601
  if (args.length === 0) {
598
602
  logMessage(
599
603
  "error",
600
- "{white-fg}✗{/white-fg} Usage: /launch <claude|codex|agy|ucode> [nickname=<name>] [profile=<id>] [count=<n>] [scope=inplace|window]"
604
+ "{white-fg}✗{/white-fg} Usage: /launch <claude|codex|agy|kimi|ucode> [nickname=<name>] [profile=<id>] [count=<n>] [scope=inplace|window]"
601
605
  );
602
606
  return;
603
607
  }
@@ -606,8 +610,9 @@ function createCommandExecutor(options = {}) {
606
610
  // Accept friendly aliases the same way `ufoo launch` does in cli.js.
607
611
  let agentType = agentTypeInput;
608
612
  if (agentTypeInput === "antigravity" || agentTypeInput === "uagy") agentType = "agy";
609
- if (agentType !== "claude" && agentType !== "codex" && agentType !== "agy" && agentType !== "ucode") {
610
- logMessage("error", "{white-fg}✗{/white-fg} Unknown agent type. Use: claude, codex, agy, or ucode");
613
+ if (agentTypeInput === "kimi-cli" || agentTypeInput === "kimi-code" || agentTypeInput === "ukimi") agentType = "kimi";
614
+ if (agentType !== "claude" && agentType !== "codex" && agentType !== "agy" && agentType !== "kimi" && agentType !== "ucode") {
615
+ logMessage("error", "{white-fg}✗{/white-fg} Unknown agent type. Use: claude, codex, agy, kimi, or ucode");
611
616
  return;
612
617
  }
613
618
  const normalizedAgent = agentType === "ucode" ? "ufoo" : agentType;
@@ -1380,11 +1385,11 @@ function createCommandExecutor(options = {}) {
1380
1385
  return;
1381
1386
  }
1382
1387
 
1383
- if (action === "codex" || action === "claude" || action === "agy") {
1388
+ if (action === "codex" || action === "claude" || action === "agy" || action === "kimi") {
1384
1389
  const kv = parseKeyValueArgs(args.slice(1));
1385
1390
  const provider = action === "claude"
1386
1391
  ? "claude-cli"
1387
- : (action === "agy" ? "agy-cli" : "codex-cli");
1392
+ : (action === "agy" ? "agy-cli" : (action === "kimi" ? "kimi-cli" : "codex-cli"));
1388
1393
  const model = String(kv.model || defaultAgentModelForProvider(provider)).trim();
1389
1394
  saveConfig(projectRoot, {
1390
1395
  agentProvider: provider,
@@ -228,6 +228,7 @@ function normalizeAgentLabel(value = "") {
228
228
  if (raw === "claude" || raw === "uclaude") return "claude";
229
229
  if (raw === "codex" || raw === "ucodex") return "codex";
230
230
  if (raw === "agy" || raw === "antigravity" || raw === "uagy") return "agy";
231
+ if (raw === "kimi" || raw === "kimi-cli" || raw === "kimi-code" || raw === "ukimi") return "kimi";
231
232
  if (raw === "ucode" || raw === "ufoo") return "ufoo";
232
233
  return raw || "agent";
233
234
  }
@@ -5,6 +5,7 @@ const DEFAULT_MODE_OPTIONS = ["auto", "host", "terminal", "tmux", "internal"];
5
5
  function providerLabel(value) {
6
6
  if (value === "claude-cli") return "claude";
7
7
  if (value === "agy-cli" || value === "agy" || value === "antigravity") return "agy";
8
+ if (value === "kimi-cli" || value === "kimi" || value === "kimi-code") return "kimi";
8
9
  if (value === "ucode" || value === "ufoo" || value === "ufoo-code") return "ucode";
9
10
  return "codex";
10
11
  }
@@ -2,6 +2,11 @@ const { calculatePaneLayout } = require("./paneLayout");
2
2
  const { createPaneManager } = require("./paneManager");
3
3
  const { createRenderer } = require("./renderer");
4
4
 
5
+ // Pane output bursts are coalesced into short batches; full chrome repaints
6
+ // are throttled with a trailing frame so the final state always renders.
7
+ const PANE_OUTPUT_BATCH_MS = 50;
8
+ const RENDER_ALL_MIN_INTERVAL_MS = 100;
9
+
5
10
  function createMultiWindowController(options = {}) {
6
11
  const {
7
12
  processStdout = process.stdout,
@@ -30,6 +35,9 @@ function createMultiWindowController(options = {}) {
30
35
  let active = false;
31
36
  let renderThrottleTimer = null;
32
37
  let dirtyPanes = new Set();
38
+ let renderAllTimer = null;
39
+ let renderAllTrailing = false;
40
+ let lastRenderAllAt = 0;
33
41
  let lastCompletionPopup = null;
34
42
  const renderer = createRenderer({ write: (d) => processStdout.write(d) });
35
43
  const paneManager = createPaneManager({
@@ -37,18 +45,14 @@ function createMultiWindowController(options = {}) {
37
45
  onInternalSubmit,
38
46
  onPaneOutput: (agentId) => {
39
47
  if (!active) return;
40
- if (getTerminalFocused()) {
41
- renderSinglePane(agentId);
42
- } else {
43
- dirtyPanes.add(agentId);
44
- if (!renderThrottleTimer) {
45
- renderThrottleTimer = setTimeout(() => {
46
- renderThrottleTimer = null;
47
- const panes = [...dirtyPanes];
48
- dirtyPanes.clear();
49
- for (const id of panes) renderSinglePane(id);
50
- }, 200);
51
- }
48
+ dirtyPanes.add(agentId);
49
+ if (!renderThrottleTimer) {
50
+ renderThrottleTimer = setTimeout(() => {
51
+ renderThrottleTimer = null;
52
+ const panes = [...dirtyPanes];
53
+ dirtyPanes.clear();
54
+ for (const id of panes) renderSinglePane(id);
55
+ }, PANE_OUTPUT_BATCH_MS);
52
56
  }
53
57
  },
54
58
  });
@@ -62,7 +66,7 @@ function createMultiWindowController(options = {}) {
62
66
  renderer.hideCursor();
63
67
  renderer.clear();
64
68
  syncAgents();
65
- renderAll();
69
+ renderAllNow();
66
70
  return true;
67
71
  }
68
72
 
@@ -74,6 +78,11 @@ function createMultiWindowController(options = {}) {
74
78
  renderThrottleTimer = null;
75
79
  dirtyPanes.clear();
76
80
  }
81
+ if (renderAllTimer) {
82
+ clearTimeout(renderAllTimer);
83
+ renderAllTimer = null;
84
+ renderAllTrailing = false;
85
+ }
77
86
  paneManager.disconnectAll();
78
87
  renderer.showCursor();
79
88
  restoreTerminal();
@@ -130,13 +139,41 @@ function createMultiWindowController(options = {}) {
130
139
  }
131
140
  }
132
141
 
133
- function renderAll() {
142
+ function renderAllNow() {
134
143
  if (!active) return;
144
+ lastRenderAllAt = Date.now();
135
145
  try {
136
146
  const agents = paneManager.getAgentIds();
137
147
  const layout = calculatePaneLayout(getCols(), getRows(), agents.length);
138
148
  const cols = getCols();
139
149
 
150
+ // Resolve the completion popup up front so stale rows are cleared
151
+ // before anything repaints (clearing also invalidates renderer caches).
152
+ const cmp = getCompletions();
153
+ let nextCompletionPopup = null;
154
+ let nextCompletionItems = null;
155
+ let nextCompletionStart = 0;
156
+ if (cmp && Array.isArray(cmp.items) && cmp.items.length > 0 && layout.inputPane) {
157
+ const start = Math.min(cmp.windowStart || 0, Math.max(0, cmp.items.length - (cmp.pageSize || 8)));
158
+ const end = Math.min(cmp.items.length, start + (cmp.pageSize || 8));
159
+ const visible = cmp.items.slice(start, end);
160
+ const popupTop = layout.inputPane.top - visible.length - 1;
161
+ if (popupTop >= 0) {
162
+ nextCompletionPopup = { top: popupTop, left: 0, width: cols, height: visible.length + 1 };
163
+ nextCompletionItems = visible;
164
+ nextCompletionStart = start;
165
+ }
166
+ }
167
+ const prevPopup = lastCompletionPopup;
168
+ const samePopupRect = prevPopup && nextCompletionPopup &&
169
+ prevPopup.top === nextCompletionPopup.top &&
170
+ prevPopup.width === nextCompletionPopup.width &&
171
+ prevPopup.height === nextCompletionPopup.height &&
172
+ (prevPopup.left || 0) === (nextCompletionPopup.left || 0);
173
+ if (prevPopup && !samePopupRect && typeof renderer.clearRows === "function") {
174
+ renderer.clearRows(prevPopup.top, prevPopup.height, prevPopup.width, prevPopup.left || 0);
175
+ }
176
+
140
177
  renderer.renderChatLog(layout.chatPane, getChatLogLines());
141
178
 
142
179
  const focused = getTerminalFocused() ? paneManager.getFocused() : null;
@@ -173,35 +210,19 @@ function createMultiWindowController(options = {}) {
173
210
  renderer.renderDashboard(layout.dashboardPane, lines);
174
211
  }
175
212
 
176
- const cmp = getCompletions();
177
- let nextCompletionPopup = null;
178
- if (lastCompletionPopup && typeof renderer.clearRows === "function") {
179
- renderer.clearRows(
180
- lastCompletionPopup.top,
181
- lastCompletionPopup.height,
182
- lastCompletionPopup.width,
183
- lastCompletionPopup.left || 0
184
- );
185
- }
186
- if (cmp && Array.isArray(cmp.items) && cmp.items.length > 0 && layout.inputPane) {
187
- const start = Math.min(cmp.windowStart || 0, Math.max(0, cmp.items.length - (cmp.pageSize || 8)));
188
- const end = Math.min(cmp.items.length, start + (cmp.pageSize || 8));
189
- const visible = cmp.items.slice(start, end);
190
- const popupTop = layout.inputPane.top - visible.length - 1;
191
- if (popupTop >= 0) {
192
- nextCompletionPopup = { top: popupTop, left: 0, width: cols, height: visible.length + 1 };
193
- renderer.renderSeparator({ top: popupTop, left: 0, width: cols });
194
- for (let i = 0; i < visible.length; i++) {
195
- const idx = start + i;
196
- const selected = idx === cmp.index;
197
- const label = visible[i].label || "";
198
- const desc = visible[i].description || "";
199
- const line = selected
200
- ? `\x1b[7;36m${label}\x1b[0m \x1b[90m${desc}\x1b[0m`
201
- : `\x1b[90m${label} ${desc}\x1b[0m`;
202
- const pad = Math.max(0, cols - renderer.visibleLength(line));
203
- renderer.write(renderer.moveTo(popupTop + 1 + i, 0) + line + " ".repeat(pad) + "\x1b[0m");
204
- }
213
+ if (nextCompletionPopup && nextCompletionItems) {
214
+ const popupTop = nextCompletionPopup.top;
215
+ renderer.renderSeparator({ top: popupTop, left: 0, width: cols });
216
+ for (let i = 0; i < nextCompletionItems.length; i++) {
217
+ const idx = nextCompletionStart + i;
218
+ const selected = idx === cmp.index;
219
+ const label = nextCompletionItems[i].label || "";
220
+ const desc = nextCompletionItems[i].description || "";
221
+ const line = selected
222
+ ? `\x1b[7;36m${label}\x1b[0m \x1b[90m${desc}\x1b[0m`
223
+ : `\x1b[90m${label} ${desc}\x1b[0m`;
224
+ const pad = Math.max(0, cols - renderer.visibleLength(line));
225
+ renderer.write(renderer.moveTo(popupTop + 1 + i, 0) + line + " ".repeat(pad) + "\x1b[0m");
205
226
  }
206
227
  }
207
228
  lastCompletionPopup = nextCompletionPopup;
@@ -210,6 +231,27 @@ function createMultiWindowController(options = {}) {
210
231
  }
211
232
  }
212
233
 
234
+ // Throttled entry point for external callers: renders immediately when idle,
235
+ // otherwise coalesces bursts into one trailing frame so state stays consistent.
236
+ function renderAll() {
237
+ if (!active) return;
238
+ const elapsed = Date.now() - lastRenderAllAt;
239
+ if (!renderAllTimer && elapsed >= RENDER_ALL_MIN_INTERVAL_MS) {
240
+ renderAllNow();
241
+ return;
242
+ }
243
+ renderAllTrailing = true;
244
+ if (!renderAllTimer) {
245
+ renderAllTimer = setTimeout(() => {
246
+ renderAllTimer = null;
247
+ if (!renderAllTrailing) return;
248
+ renderAllTrailing = false;
249
+ renderAllNow();
250
+ }, Math.max(RENDER_ALL_MIN_INTERVAL_MS - elapsed, 16));
251
+ if (typeof renderAllTimer.unref === "function") renderAllTimer.unref();
252
+ }
253
+ }
254
+
213
255
  function handleKey(key) {
214
256
  if (!active) return false;
215
257
 
@@ -219,7 +261,7 @@ function createMultiWindowController(options = {}) {
219
261
 
220
262
  if (key.name === "w" && key.ctrl) {
221
263
  paneManager.cycleFocus();
222
- renderAll();
264
+ renderAllNow();
223
265
  return true;
224
266
  }
225
267
 
@@ -237,14 +279,14 @@ function createMultiWindowController(options = {}) {
237
279
  if (!agents.includes(agentId)) return;
238
280
  paneManager.setFocused(agentId);
239
281
  onFocusAgent(agentId);
240
- renderAll();
282
+ renderAllNow();
241
283
  }
242
284
 
243
285
  function handleResize() {
244
286
  if (!active) return;
245
287
  syncAgents();
246
288
  renderer.clear();
247
- renderAll();
289
+ renderAllNow();
248
290
  }
249
291
 
250
292
  function isActive() { return active; }
@@ -10,10 +10,36 @@ function createRenderer(options = {}) {
10
10
  write: rawWrite = process.stdout.write.bind(process.stdout),
11
11
  } = options;
12
12
 
13
+ // Last-write caches used to skip redundant output. Both are reset by
14
+ // clear()/clearRows() so a blanked region is always repainted.
15
+ let slotCache = new Map();
16
+ let paneSigs = new WeakMap();
17
+
13
18
  function write(data) {
14
19
  try { rawWrite(data); } catch {}
15
20
  }
16
21
 
22
+ function writeIfChanged(slot, data, top, height) {
23
+ const prev = slotCache.get(slot);
24
+ if (prev && prev.data === data && prev.top === top) return;
25
+ slotCache.set(slot, { data, top, height });
26
+ write(data);
27
+ }
28
+
29
+ function paneSignature(pane, focused, extra) {
30
+ return [pane.top, pane.left, pane.width, pane.height, focused ? 1 : 0, extra || ""].join("|");
31
+ }
32
+
33
+ function shouldSkipPane(vt, sig, force) {
34
+ if (force || paneSigs.get(vt) !== sig) return false;
35
+ return typeof vt.isDirty === "function" ? !vt.isDirty() : false;
36
+ }
37
+
38
+ function markPaneRendered(vt, sig) {
39
+ paneSigs.set(vt, sig);
40
+ if (vt && typeof vt.clearDirty === "function") vt.clearDirty();
41
+ }
42
+
17
43
  function moveTo(row, col) {
18
44
  return `\x1b[${row + 1};${col + 1}H`;
19
45
  }
@@ -59,7 +85,9 @@ function createRenderer(options = {}) {
59
85
  );
60
86
  }
61
87
 
62
- function renderPane(vt, pane, focused, label) {
88
+ function renderPane(vt, pane, focused, label, opts = {}) {
89
+ const sig = paneSignature(pane, focused, label || "");
90
+ if (shouldSkipPane(vt, sig, opts.force)) return;
63
91
  const { buffer, rows, cols, cursorRow, cursorCol } = vt.getScreen();
64
92
  const { top, left, width, height } = pane;
65
93
 
@@ -108,6 +136,7 @@ function createRenderer(options = {}) {
108
136
  out += moveTo(top + height - 1, left) + borderColor + botLine + reset;
109
137
 
110
138
  write(out);
139
+ markPaneRendered(vt, sig);
111
140
  }
112
141
 
113
142
  function renderCells(cells, maxWidth, cursorCol = -1) {
@@ -147,7 +176,13 @@ function createRenderer(options = {}) {
147
176
  return `${color}${truncated}${" ".repeat(pad)}${reset}`;
148
177
  }
149
178
 
150
- function renderInternalPane(vt, pane, focused, info = {}) {
179
+ function renderInternalPane(vt, pane, focused, info = {}, opts = {}) {
180
+ const extra = JSON.stringify([
181
+ info.label || "", info.status || "", info.detail || "",
182
+ info.input || "", info.cursor ?? "",
183
+ ]);
184
+ const sig = paneSignature(pane, focused, extra);
185
+ if (shouldSkipPane(vt, sig, opts.force)) return;
151
186
  const { buffer, rows, cols } = vt.getScreen();
152
187
  const { top, left, width, height } = pane;
153
188
 
@@ -217,6 +252,7 @@ function createRenderer(options = {}) {
217
252
  out += moveTo(top + height - 1, left) + borderColor + botLine + reset;
218
253
 
219
254
  write(out);
255
+ markPaneRendered(vt, sig);
220
256
  }
221
257
 
222
258
  function stripControl(str) {
@@ -288,14 +324,14 @@ function createRenderer(options = {}) {
288
324
  out += truncated + reset + " ".repeat(pad);
289
325
  out += dim + BOX.v + reset;
290
326
  }
291
- write(out);
327
+ writeIfChanged("chatlog", out, top, height);
292
328
  }
293
329
 
294
330
  function renderSeparator(pane, highlighted) {
295
331
  const { top, left, width } = pane;
296
332
  const reset = "\x1b[0m";
297
333
  const color = highlighted ? "\x1b[36m" : "\x1b[90m";
298
- write(moveTo(top, left) + color + "─".repeat(width) + reset);
334
+ writeIfChanged(`sep:${top}:${left}`, moveTo(top, left) + color + "─".repeat(width) + reset, top, 1);
299
335
  }
300
336
 
301
337
  function renderStatusLine(pane, text) {
@@ -304,17 +340,19 @@ function createRenderer(options = {}) {
304
340
  const dim = "\x1b[90m";
305
341
  const truncated = truncateVisible(text || "", width);
306
342
  const pad = Math.max(0, width - visibleLength(truncated));
307
- write(moveTo(top, left) + dim + truncated + " ".repeat(pad) + reset);
343
+ writeIfChanged("status", moveTo(top, left) + dim + truncated + " ".repeat(pad) + reset, top, 1);
308
344
  }
309
345
 
310
346
  function renderDashboard(pane, lines) {
311
347
  const { top, left, width } = pane;
312
348
  const reset = "\x1b[0m";
349
+ let out = "";
313
350
  for (let i = 0; i < lines.length; i++) {
314
351
  const line = lines[i] || "";
315
352
  const pad = Math.max(0, width - visibleLength(line));
316
- write(moveTo(top + i, left) + line + " ".repeat(pad) + reset);
353
+ out += moveTo(top + i, left) + line + " ".repeat(pad) + reset;
317
354
  }
355
+ writeIfChanged("dashboard", out, top, lines.length);
318
356
  }
319
357
 
320
358
  function renderInputPrompt(pane, prefix, draft, cursor) {
@@ -331,17 +369,28 @@ function createRenderer(options = {}) {
331
369
  const promptLine = cyan + prefixStr + reset + before + inverse + cursorChar + reset + after;
332
370
  const truncated = truncateVisible(promptLine, width);
333
371
  const pad = Math.max(0, width - visibleLength(truncated));
334
- write(moveTo(top, left) + truncated + " ".repeat(pad) + reset);
372
+ writeIfChanged("input", moveTo(top, left) + truncated + " ".repeat(pad) + reset, top, 1);
335
373
  }
336
374
 
337
375
  function hideCursor() { write("\x1b[?25l"); }
338
376
  function showCursor() { write("\x1b[?25h"); }
339
- function clear() { write("\x1b[2J\x1b[H"); }
377
+ function clear() {
378
+ slotCache = new Map();
379
+ paneSigs = new WeakMap();
380
+ write("\x1b[2J\x1b[H");
381
+ }
340
382
 
341
383
  function clearRows(top, count, width, left = 0) {
342
384
  const rows = Math.max(0, Number(count) || 0);
343
385
  const cols = Math.max(0, Number(width) || 0);
344
386
  if (rows === 0 || cols === 0) return;
387
+ // Blanked rows must be repainted on the next pass: drop every cached
388
+ // slot overlapping the cleared range and reset pane signatures (kept
389
+ // in a WeakMap, so they cannot be filtered selectively).
390
+ for (const [slot, entry] of slotCache) {
391
+ if (entry.top < top + rows && top < entry.top + entry.height) slotCache.delete(slot);
392
+ }
393
+ paneSigs = new WeakMap();
345
394
  let out = "";
346
395
  const blank = " ".repeat(cols);
347
396
  for (let i = 0; i < rows; i++) {
@@ -733,8 +733,8 @@ async function runCli(argv) {
733
733
 
734
734
  program
735
735
  .command("launch")
736
- .description("Launch an agent (uclaude, ucodex, uagy, ucode)")
737
- .argument("<agent>", "Agent type: uclaude|ucodex|uagy|ucode|claude|codex|agy")
736
+ .description("Launch an agent (uclaude, ucodex, uagy, ukimi, ucode)")
737
+ .argument("<agent>", "Agent type: uclaude|ucodex|uagy|ukimi|ucode|claude|codex|agy|kimi")
738
738
  .argument("[nickname]", "Optional nickname for the agent")
739
739
  .option("--profile <id>", "Prompt profile to assign after launch")
740
740
  .action(async (agent, nickname, opts) => {
@@ -751,11 +751,13 @@ async function runCli(argv) {
751
751
  normalizedAgent = "codex";
752
752
  } else if (agentLower === "uagy" || agentLower === "agy" || agentLower === "antigravity") {
753
753
  normalizedAgent = "agy";
754
+ } else if (agentLower === "ukimi" || agentLower === "kimi" || agentLower === "kimi-cli" || agentLower === "kimi-code") {
755
+ normalizedAgent = "kimi";
754
756
  } else if (agentLower === "ucode" || agentLower === "ufoo-code" || agentLower === "ufoo") {
755
757
  normalizedAgent = "ucode";
756
758
  } else {
757
759
  console.error(`Unknown agent type: ${agent}`);
758
- console.error("Valid types: uclaude, ucodex, uagy, ucode, claude, codex, agy");
760
+ console.error("Valid types: uclaude, ucodex, uagy, ukimi, ucode, claude, codex, agy, kimi");
759
761
  process.exitCode = 1;
760
762
  return;
761
763
  }