@noobdemon/noob-cli 1.12.15 → 1.12.17

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 CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  Tất cả thay đổi đáng kể của `@noobdemon/noob-cli` được ghi vào file này.
4
4
 
5
+ ## [1.12.17] - 2026-06-24
6
+
7
+ ### Added
8
+ - **Image input support** (`src/tui.js`, `src/repl.js`, `src/api.js`, `src/agent.js`, `worker/src/worker.js`): rich TTY trên Windows hỗ trợ `Alt+V` để lấy ảnh từ clipboard, hiện chip `[pasted image #1]`, rồi gửi kèm payload `image: "data:image/png;base64,..."` qua gateway. CLI cũng tự đính kèm ảnh đầu tiên khi user nhắc `@file.png|jpg|jpeg|webp|gif` (giới hạn 8MB). Worker validate data URL và forward `image` sang Railway (`{message, model, image}`); fallback cũ giữ `{message, model}`.
9
+
10
+ ### Verified
11
+ - `npm test` 100/100 pass.
12
+ - Railway `/chat` smoke với `image` data URL base64 trả mô tả ảnh thành công.
13
+
5
14
  ## [1.12.15] - 2026-06-16
6
15
 
7
16
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.12.15",
3
+ "version": "1.12.17",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/agent.js CHANGED
@@ -170,6 +170,25 @@ function goalBlock(goal) {
170
170
  ].join('\n');
171
171
  }
172
172
 
173
+ // Mode hint (build/plan/compose) — chỉ NHẮC mềm, KHÔNG hard-gate tool. build =
174
+ // default (full, không thêm khối). plan/compose chèn 1 đoạn hướng dẫn nhẹ vào
175
+ // system prompt để model điều chỉnh phong cách làm việc.
176
+ function modeBlock(uiMode) {
177
+ if (uiMode === 'plan') {
178
+ return [
179
+ '# CHẾ ĐỘ PLAN',
180
+ 'Ưu tiên phân tích & đọc code trước. Đề xuất kế hoạch rõ ràng TRƯỚC khi sửa file. Tránh edit_file/write_file/run_command (thao tác thay đổi) trừ khi user đã xác nhận hướng đi. Mục tiêu: hiểu sâu & lên kế hoạch, không vội thực thi.',
181
+ ].join('\n');
182
+ }
183
+ if (uiMode === 'compose') {
184
+ return [
185
+ '# CHẾ ĐỘ COMPOSE',
186
+ 'Làm việc theo workflow có cấu trúc: (1) làm rõ specs/yêu cầu → (2) lập kế hoạch → (3) thực thi từng bước → (4) báo cáo kết quả. Giữ kỷ luật từng giai đoạn, không nhảy cóc.',
187
+ ].join('\n');
188
+ }
189
+ return '';
190
+ }
191
+
173
192
  // Môi trường chạy thực: model cần biết OS + shell để emit lệnh ĐÚNG. Không có
174
193
  // khối này, trên Windows model hay emit lệnh Unix (wc/ls/cat/grep) → run_command
175
194
  // (PowerShell) báo lỗi.
@@ -417,7 +436,7 @@ function relTime(ts) {
417
436
  // chèn ngay sau SYSTEM để model biết và dùng được.
418
437
  // recentSessions: breadcrumbs các phiên trước cùng workspace (repl.js cung cấp)
419
438
  // → chèn ngay sau memoryBlock() để model "thấy" lịch sử dù chưa /resume.
420
- export function buildSystem(history, extraToolsDoc, goal, recentSessions) {
439
+ export function buildSystem(history, extraToolsDoc, goal, recentSessions, uiMode) {
421
440
  const parts = [SYSTEM, '', memoryBlock()];
422
441
  // Auto-active skills (frontmatter `auto: true`) — luôn ON, không cần slash.
423
442
  // Đặt sau memoryBlock để model thấy convention skill TRƯỚC khi vào tool/goal.
@@ -427,6 +446,8 @@ export function buildSystem(history, extraToolsDoc, goal, recentSessions) {
427
446
  parts.push('', recentSessionsBlock(recentSessions));
428
447
  }
429
448
  if (goal && goal.trim()) parts.push('', goalBlock(goal));
449
+ const mode = modeBlock(uiMode);
450
+ if (mode) parts.push('', mode);
430
451
  if (extraToolsDoc) parts.push('', extraToolsDoc);
431
452
  parts.push('', runtimeContext());
432
453
  return parts.join('\n');
@@ -535,6 +556,7 @@ function extractJsonObject(s, from) {
535
556
  export async function runAgent({
536
557
  history,
537
558
  model,
559
+ image,
538
560
  signal,
539
561
  onTool,
540
562
  onStatus,
@@ -545,6 +567,7 @@ export async function runAgent({
545
567
  goal,
546
568
  recentSessions,
547
569
  pendingTasks,
570
+ uiMode,
548
571
  }) {
549
572
  // [GỠ BUDGET 2026-06-06] Không còn token budget enforcement. Agent/loop/sub-agent
550
573
  // chạy không giới hạn token. Dừng theo: GOAL đạt, <<LOOP_DONE>>, <<ULTRA_DONE>>,
@@ -573,7 +596,7 @@ export async function runAgent({
573
596
  await maybeSummarize(history, { model, signal });
574
597
  } catch {}
575
598
 
576
- const system = buildSystem(history, extraToolsDoc, goal, recentSessions);
599
+ const system = buildSystem(history, extraToolsDoc, goal, recentSessions, uiMode);
577
600
  const message = buildUserMessage(history);
578
601
  tokenMeter?.addInput(countTokens(system) + countTokens(message));
579
602
  tokenMeter?.setContext(tokenMeter.total);
@@ -586,6 +609,7 @@ export async function runAgent({
586
609
  const { text, finishReason } = await streamWithRetry({
587
610
  model,
588
611
  message,
612
+ image,
589
613
  system,
590
614
  signal,
591
615
  tokenMeter,
@@ -734,6 +758,7 @@ export async function runAgent({
734
758
  async function streamWithRetry({
735
759
  model,
736
760
  message,
761
+ image,
737
762
  system,
738
763
  signal,
739
764
  tokenMeter,
@@ -749,6 +774,7 @@ async function streamWithRetry({
749
774
  mode: 'chat',
750
775
  model,
751
776
  message,
777
+ image,
752
778
  system,
753
779
  signal,
754
780
  effort,
package/src/api.js CHANGED
@@ -157,6 +157,7 @@ function hasUnclosedToolBlock(text) {
157
157
  export async function stream({
158
158
  mode = 'chat',
159
159
  message,
160
+ image,
160
161
  model,
161
162
  system,
162
163
  conversation,
@@ -186,6 +187,7 @@ export async function stream({
186
187
  endpoint,
187
188
  mode,
188
189
  message: prompt,
190
+ image,
189
191
  model,
190
192
  system,
191
193
  conversation,
@@ -262,6 +264,7 @@ async function streamOnce({
262
264
  endpoint,
263
265
  mode,
264
266
  message,
267
+ image,
265
268
  model,
266
269
  system,
267
270
  conversation,
@@ -278,6 +281,7 @@ async function streamOnce({
278
281
  else if (mode === 'merge') body = { message };
279
282
  else {
280
283
  body = { message, model, remember: true, memoryToken: getMemoryToken() };
284
+ if (image) body.image = image;
281
285
  if (system) body.customInstructions = system;
282
286
  if (Array.isArray(conversation) && conversation.length) body.conversation = conversation;
283
287
  if (effort) body.effort = effort;
@@ -22,6 +22,7 @@ export const SLASH = [
22
22
  { name: '/improve', desc: 'phân tích workspace & gợi ý tính năng cải thiện' },
23
23
  { name: '/ultra', desc: 'tự hành: tự nghĩ & làm nhiệm vụ' },
24
24
  { name: '/agent', desc: 'bật/tắt agent mode (spawn sub-agent)' },
25
+ { name: '/mode', desc: 'build|plan|compose — đổi chế độ agent (Ctrl+T cycle)' },
25
26
  { name: '/goal', desc: 'đặt HARD GOAL — model phải hướng tới tới khi /goal clear' },
26
27
  { name: '/loop', desc: 'chạy task định kỳ (vd: /loop 5m kiểm tra log) · /loop stop để dừng' },
27
28
  { name: '/tokens', desc: 'xem số token đã dùng phiên này' },
package/src/repl/state.js CHANGED
@@ -18,6 +18,7 @@ export function createState(opts = {}, config) {
18
18
  return {
19
19
  model: findModel(opts.model) || findModel(config.model) || findModel(DEFAULT_MODEL),
20
20
  mode: 'chat', // chat | merge | search
21
+ agentUiMode: 'build', // build | plan | compose — UI hint mode (Ctrl+T / /mode). KHÔNG đụng `mode`.
21
22
  history: [],
22
23
  autoApprove: new Set(), // tool name → 'a' (always, phiên)
23
24
  autoApproveTurn: new Set(), // tool name → 't' (this turn, reset sau mỗi runAgent)
package/src/repl.js CHANGED
@@ -101,6 +101,24 @@ import {
101
101
  } from './repl/utils.js';
102
102
  import { createAgentDispatcher } from './repl/agent-dispatch.js';
103
103
  import { createBgRegistry } from './workflow-bg.js';
104
+
105
+ const IMAGE_MIME = {
106
+ '.png': 'image/png',
107
+ '.jpg': 'image/jpeg',
108
+ '.jpeg': 'image/jpeg',
109
+ '.webp': 'image/webp',
110
+ '.gif': 'image/gif',
111
+ };
112
+
113
+ function imageDataUrl(file) {
114
+ if (!file) return null;
115
+ const mime = IMAGE_MIME[path.extname(file).toLowerCase()];
116
+ if (!mime) return null;
117
+ const abs = path.resolve(process.cwd(), file);
118
+ const stat = fs.statSync(abs);
119
+ if (stat.size > 8 * 1024 * 1024) throw new Error('Ảnh quá lớn (>8MB): ' + file);
120
+ return `data:${mime};base64,${fs.readFileSync(abs).toString('base64')}`;
121
+ }
104
122
  export async function startRepl(opts = {}) {
105
123
  const state = createState(opts, config);
106
124
  const tokenMeter = new TokenMeter();
@@ -160,6 +178,14 @@ export async function startRepl(opts = {}) {
160
178
  tui.print(state.yolo ? c.err(' ' + t.yoloOn) : c.ok(' ' + t.yoloOff));
161
179
  tui.setPrompt(promptStr(false));
162
180
  },
181
+ onCtrlT: () => {
182
+ // Cycle build → plan → compose. UI hint mode (chỉ system-prompt hint).
183
+ const order = ['build', 'plan', 'compose'];
184
+ const next = order[(order.indexOf(state.agentUiMode) + 1) % order.length];
185
+ state.agentUiMode = next;
186
+ tui.setAgentMode(next);
187
+ tui.print(c.accent(' ◆ mode: ') + next);
188
+ },
163
189
  completer: completeInput,
164
190
  });
165
191
 
@@ -338,6 +364,7 @@ export async function startRepl(opts = {}) {
338
364
  state.history = s.history || [];
339
365
  state.mode = 'chat';
340
366
  state.goal = s.goal || null; // khôi phục HARD GOAL nếu phiên cũ có
367
+ tui.setGoal(state.goal || '');
341
368
  if (s.model) {
342
369
  const m = findModel(s.model);
343
370
  if (m) state.model = m;
@@ -1318,6 +1345,8 @@ NGUYÊN TẮC:
1318
1345
  }
1319
1346
 
1320
1347
  tui.start();
1348
+ tui.setAgentMode(state.agentUiMode);
1349
+ tui.setGoal(state.goal || '');
1321
1350
  banner();
1322
1351
  printStatus(state);
1323
1352
  // noob.md status line — cho user thấy model có memory gì (số dòng, rules/notes,
@@ -1442,23 +1471,25 @@ NGUYÊN TẮC:
1442
1471
  let input;
1443
1472
  if (pending.length) {
1444
1473
  // Có tin xếp hàng → tự gửi câu kế tiếp (không chờ gõ).
1445
- input = (pending.shift() ?? '').trim();
1474
+ input = { text: (pending.shift() ?? '').trim(), images: [] };
1446
1475
  } else {
1447
1476
  const raw = await ask(promptStr(false));
1448
1477
  if (raw == null) break; // stdin fully closed and drained
1449
- input = raw.trim();
1478
+ input = typeof raw === 'string' ? { text: raw.trim(), images: [] } : raw;
1479
+ input.text = String(input.text || '').trim();
1480
+ input.images = Array.isArray(input.images) ? input.images : [];
1450
1481
  }
1451
- if (!input) continue;
1482
+ if (!input.text) continue;
1452
1483
  // Bọc cả lượt: một lỗi trong xử lý lệnh/agent không được phép thoát ra
1453
1484
  // ngoài vòng lặp (sẽ rơi vào .catch ở bin/noob.js → process.exit(1) =
1454
1485
  // "tự động tắt"). Bắt ở đây, in lỗi, rồi tiếp tục vòng lặp.
1455
1486
  try {
1456
- if (input.startsWith('/')) {
1457
- const done = await command(input);
1487
+ if (input.text.startsWith('/')) {
1488
+ const done = await command(input.text);
1458
1489
  if (done) break;
1459
1490
  continue;
1460
1491
  }
1461
- await handle(input);
1492
+ await handle(input.text, { images: input.images });
1462
1493
  persist(); // lưu sau mỗi lượt → resume được kể cả khi tắt đột ngột
1463
1494
  } catch (err) {
1464
1495
  printError(err);
@@ -1470,7 +1501,7 @@ NGUYÊN TẮC:
1470
1501
  process.exit(0);
1471
1502
 
1472
1503
  // ── turn handler ─────────────────────────────────────────────────────────
1473
- async function handle(text) {
1504
+ async function handle(text, opts = {}) {
1474
1505
  if (!config.apiKey) {
1475
1506
  console.log(c.tool(' ' + t.notLoggedIn));
1476
1507
  return;
@@ -1535,9 +1566,12 @@ NGUYÊN TẮC:
1535
1566
  }
1536
1567
 
1537
1568
  const files = mentionedFiles(text);
1569
+ const image =
1570
+ opts.images?.[0] ||
1571
+ imageDataUrl(files.find((f) => IMAGE_MIME[path.extname(f).toLowerCase()]));
1538
1572
  const content = files.length
1539
1573
  ? text +
1540
- `\n\n[File người dùng nhắc tới bằng @: ${files.join(', ')} — đọc bằng read_file nếu cần.]`
1574
+ `\n\n[File người dùng nhắc tới bằng @: ${files.join(', ')} — đọc bằng read_file nếu cần.${image ? ' Ảnh đầu tiên đã được đính kèm cho model vision.' : ''}]`
1541
1575
  : text;
1542
1576
  state.history.push({ role: 'user', content });
1543
1577
  // Update terminal title với session name (trích từ message đầu).
@@ -1550,7 +1584,8 @@ NGUYÊN TẮC:
1550
1584
  state.history,
1551
1585
  state.agentMode ? spawnAgentToolsDoc(0) : '',
1552
1586
  state.goal,
1553
- sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id)
1587
+ sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id),
1588
+ state.agentUiMode
1554
1589
  );
1555
1590
  const userMessage = buildUserMessage(state.history);
1556
1591
  tokenMeter.setContext(countTokens(systemPrompt) + countTokens(userMessage));
@@ -1580,11 +1615,13 @@ NGUYÊN TẮC:
1580
1615
  const answer = await runAgent({
1581
1616
  history: state.history,
1582
1617
  model: state.model.id,
1618
+ image,
1583
1619
  signal: abort.signal,
1584
1620
  tokenMeter,
1585
1621
  goal: state.goal,
1586
1622
  recentSessions: sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id),
1587
1623
  extraToolsDoc: state.agentMode ? spawnAgentToolsDoc(0) : '',
1624
+ uiMode: state.agentUiMode,
1588
1625
  // Pending tasks: todo items chưa hoàn thành từ lượt trước → model tiếp tục ngay.
1589
1626
  pendingTasks: (state.todos || []).filter((t) => !t.done).map((t) => t.text),
1590
1627
  onStatus: () => tick(t.thinking),
@@ -1874,6 +1911,20 @@ NGUYÊN TẮC:
1874
1911
  );
1875
1912
  break;
1876
1913
  }
1914
+ case 'mode': {
1915
+ const v = arg.toLowerCase().trim();
1916
+ if (!v) {
1917
+ console.log(c.accent(' ◆ mode: ') + state.agentUiMode);
1918
+ console.log(c.dim(' cú pháp: /mode build|plan|compose · hoặc Ctrl+T để cycle'));
1919
+ } else if (v === 'build' || v === 'plan' || v === 'compose') {
1920
+ state.agentUiMode = v;
1921
+ tui.setAgentMode(v);
1922
+ console.log(c.accent(' ◆ mode: ') + v);
1923
+ } else {
1924
+ console.log(c.err(' mode không hợp lệ: ') + v + c.dim(' (chọn build|plan|compose)'));
1925
+ }
1926
+ break;
1927
+ }
1877
1928
  case 'kg': {
1878
1929
  // Knowledge graph CRUD — port từ mcp-knowledge-graph, lưu .noob/kg.jsonl.
1879
1930
  // Sub-cmd: list, path, add, obs, link, unlink, unobs, get, search, rm.
@@ -2012,10 +2063,12 @@ NGUYÊN TẮC:
2012
2063
  v.toLowerCase() === 'xoa'
2013
2064
  ) {
2014
2065
  state.goal = null;
2066
+ tui.setGoal('');
2015
2067
  console.log(c.dim(' đã xoá goal'));
2016
2068
  persist();
2017
2069
  } else {
2018
2070
  state.goal = v;
2071
+ tui.setGoal(v);
2019
2072
  console.log(c.accent(' 🎯 đã đặt goal: ') + v);
2020
2073
  persist();
2021
2074
  }
package/src/tui.js CHANGED
@@ -7,11 +7,32 @@
7
7
  // Bật/tắt: TTY thật → chế độ giàu; không phải TTY hoặc NOOB_TUI=0 → chế độ "dumb"
8
8
  // (đọc dòng đơn giản, in thẳng) để khỏi vỡ ở terminal lạ / pipe / CI.
9
9
  import readline from 'node:readline';
10
+ import { execFileSync } from 'node:child_process';
10
11
  import { c } from './ui.js';
11
12
 
12
13
  const ESC = '\x1b';
13
14
  const ANSI_RE = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
14
15
  const visLen = (s) => s.replace(ANSI_RE, '').length;
16
+ function readClipboardImageDataUrl() {
17
+ if (process.platform !== 'win32') return null;
18
+ try {
19
+ const script =
20
+ 'Add-Type -AssemblyName System.Windows.Forms,System.Drawing;' +
21
+ '$img=[Windows.Forms.Clipboard]::GetImage();' +
22
+ 'if($null -eq $img){exit 2};' +
23
+ '$ms=New-Object IO.MemoryStream;' +
24
+ '$img.Save($ms,[Drawing.Imaging.ImageFormat]::Png);' +
25
+ '[Convert]::ToBase64String($ms.ToArray())';
26
+ const b64 = execFileSync('powershell.exe', ['-NoProfile', '-STA', '-Command', script], {
27
+ encoding: 'utf8',
28
+ timeout: 5000,
29
+ windowsHide: true,
30
+ }).trim();
31
+ return b64 ? `data:image/png;base64,${b64}` : null;
32
+ } catch {
33
+ return null;
34
+ }
35
+ }
15
36
  // Trả index trong `text` mà tại đó vị trí VISUAL đạt `targetVis`. Bỏ qua toàn
16
37
  // bộ ANSI escape sequence khi đếm. Dùng cho soft-wrap khi text có màu.
17
38
  function findVisPos(text, targetVis) {
@@ -107,7 +128,7 @@ function truncStr(s, max) {
107
128
  return s.length > max ? s.slice(0, max - 1) + '…' : s;
108
129
  }
109
130
 
110
- export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer } = {}) {
131
+ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, onCtrlT, completer } = {}) {
111
132
  const tty = !!(process.stdout.isTTY && process.stdin.isTTY) && process.env.NOOB_TUI !== '0';
112
133
  const cols = () => process.stdout.columns || 80;
113
134
 
@@ -154,6 +175,8 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
154
175
  setMeta() {},
155
176
  setTodos() {},
156
177
  setUltra() {},
178
+ setAgentMode() {},
179
+ setGoal() {},
157
180
  setBgAgents() {},
158
181
  pushAgentLog() {},
159
182
  clearAgentLog() {},
@@ -186,6 +209,8 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
186
209
  let todos = []; // [{text, done}] — danh sách todo đang chạy, repl parse từ model output
187
210
  let ultra = false; // ULTRA mode đang bật? → segment trong status bar
188
211
  let bgAgents = 0; // số sub-agent / workflow chạy nền → segment + gợi ý Ctrl+O
212
+ let agentMode = ''; // build | plan | compose — UI hint mode → segment status bar (Ctrl+T / /mode)
213
+ let goalText = ''; // HARD GOAL hiện tại (/goal) → segment 🎯 status bar (truncate)
189
214
  // Ring buffer log sub-agent nền: KHÔNG in ra UI chính, chỉ vào đây. Ctrl+O mở
190
215
  // overlay xem. Mỗi entry: chuỗi đã format (có thể kèm màu). Giữ tối đa MAX_LOG.
191
216
  const MAX_AGENT_LOG = 500;
@@ -203,6 +228,7 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
203
228
  // vị trí con trỏ → hỗ trợ ←/→/Home/End/Delete, chèn & xoá GIỮA dòng.
204
229
  let cells = [];
205
230
  let cur = 0;
231
+ let imageSeq = 0;
206
232
  let waiter = null;
207
233
  const queue = [];
208
234
 
@@ -250,8 +276,13 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
250
276
  ? firstLine.slice(0, PASTE_PREVIEW_MAX - 1) + '…'
251
277
  : firstLine;
252
278
  };
253
- const cellStr = (x) => (x.paste !== undefined ? x.paste : x.c);
279
+ const imageLabel = (x) => `[pasted image #${x.n}]`;
280
+ const cellStr = (x) => {
281
+ if (x.image) return imageLabel(x);
282
+ return x.paste !== undefined ? x.paste : x.c;
283
+ };
254
284
  const cellPlain = (x) => {
285
+ if (x.image) return imageLabel(x);
255
286
  if (x.paste === undefined) return x.c;
256
287
  const preview = pastePreview(x.paste);
257
288
  return preview ? `[pasted ${x.lines} lines: "${preview}"]` : `[pasted ${x.lines} lines]`;
@@ -260,6 +291,7 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
260
291
  const coloredInput = () =>
261
292
  cells
262
293
  .map((x) => {
294
+ if (x.image) return c.dim(imageLabel(x));
263
295
  if (x.paste === undefined) return x.c;
264
296
  const preview = pastePreview(x.paste);
265
297
  const label = preview
@@ -368,9 +400,11 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
368
400
  const spin = FRAMES[frame % FRAMES.length];
369
401
  // Badge ghép vào mọi dòng status: ULTRA bật/tắt + số sub-agent nền (gợi ý
370
402
  // Ctrl+O xem log). Reset ANSI từng đoạn (chalk tự đóng) → không bleed.
403
+ const modeBadge = agentMode ? c.tool(`◆ ${agentMode}`) : '';
404
+ const goalBadge = goalText ? c.accent('🎯 ') + c.dim(truncStr(goalText, 28)) : '';
371
405
  const ultraBadge = ultra ? c.err('🚀 ULTRA') : '';
372
406
  const bgBadge = bgAgents ? c.accent(`⊕ ${bgAgents} nền`) + c.dim(' (Ctrl+O)') : '';
373
- const badges = [ultraBadge, bgBadge].filter(Boolean).join(c.dim(' · '));
407
+ const badges = [modeBadge, goalBadge, ultraBadge, bgBadge].filter(Boolean).join(c.dim(' · '));
374
408
  const badgeTail = badges ? c.dim(' · ') + badges : '';
375
409
  // Todo progress: 1 dòng gọn dùng chung (📋 done/total bar pct%). Dựng SẴN ở
376
410
  // đây để ghép vào status line theo trạng thái — KHÔNG return sớm nuốt status.
@@ -562,6 +596,18 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
562
596
  histPos = null;
563
597
  } else pushText(content); // paste 1 dòng = gõ thẳng
564
598
  }
599
+ function pushImage(image) {
600
+ imageSeq += 1;
601
+ cells.splice(cur, 0, { image, n: imageSeq });
602
+ cur += 1;
603
+ histPos = null;
604
+ }
605
+ function pasteClipboardImage() {
606
+ const image = readClipboardImageDataUrl();
607
+ if (image) pushImage(image);
608
+ else w('\x07');
609
+ draw();
610
+ }
565
611
  function backspace() {
566
612
  if (cur > 0) {
567
613
  cells.splice(cur - 1, 1);
@@ -576,7 +622,8 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
576
622
  }
577
623
  }
578
624
  // null = ô dán (coi như ranh giới từ); ngược lại trả ký tự của ô.
579
- const charAt = (k) => (cells[k] && cells[k].paste === undefined ? cells[k].c : null);
625
+ const charAt = (k) =>
626
+ cells[k] && cells[k].paste === undefined && !cells[k].image ? cells[k].c : null;
580
627
  function moveWordLeft() {
581
628
  while (cur > 0 && charAt(cur - 1) === ' ') cur -= 1;
582
629
  if (cur > 0 && charAt(cur - 1) === null) return void (cur -= 1); // qua 1 chip
@@ -633,6 +680,7 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
633
680
 
634
681
  function submit() {
635
682
  const full = fullText();
683
+ const images = cells.filter((x) => x.image).map((x) => x.image);
636
684
  const echo = echoUserBlock();
637
685
  if (full.trim() && submitHistory[submitHistory.length - 1] !== full) {
638
686
  submitHistory.push(full);
@@ -648,9 +696,9 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
648
696
  if (waiter) {
649
697
  const wr = waiter;
650
698
  waiter = null;
651
- wr(full);
652
- } else if (onLine) onLine(full);
653
- else queue.push(full);
699
+ wr(images.length ? { text: full, images } : full);
700
+ } else if (onLine) onLine(images.length ? { text: full, images } : full);
701
+ else queue.push(images.length ? { text: full, images } : full);
654
702
  }
655
703
 
656
704
  let carry = '';
@@ -696,6 +744,11 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
696
744
  }
697
745
  const ch = s[i];
698
746
  if (ch === ESC) {
747
+ if (rest[1] === 'v' || rest[1] === 'V') {
748
+ pasteClipboardImage();
749
+ i += 2;
750
+ continue;
751
+ }
699
752
  // Một chuỗi CSI hoàn chỉnh: \x1b[ … <chữ/~> hoặc SS3: \x1bO<chữ>.
700
753
  const csi = rest.match(/^\x1b\[[0-9;]*[~A-Za-z]/) || rest.match(/^\x1bO[A-Za-z]/);
701
754
  if (!csi) {
@@ -800,6 +853,11 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
800
853
  i++;
801
854
  continue;
802
855
  } // Ctrl+O: bật/tắt overlay log sub-agent nền
856
+ if (ch === '\x14') {
857
+ onCtrlT?.();
858
+ i++;
859
+ continue;
860
+ } // Ctrl+T: cycle build/plan/compose mode
803
861
  if (ch === '\x01') {
804
862
  cur = 0;
805
863
  draw();
@@ -933,6 +991,14 @@ export function createTui({ onLine, onInterrupt, onEOF, onShiftTab, completer }
933
991
  ultra = !!on;
934
992
  draw();
935
993
  },
994
+ setAgentMode(mode) {
995
+ agentMode = mode || '';
996
+ draw();
997
+ },
998
+ setGoal(text) {
999
+ goalText = text || '';
1000
+ draw();
1001
+ },
936
1002
  setBgAgents(n) {
937
1003
  bgAgents = Math.max(0, n | 0);
938
1004
  draw();
package/src/ui.js CHANGED
@@ -53,6 +53,7 @@ export function banner() {
53
53
  const tag = chalk.hex('#e8e3d8').italic(t.tagline);
54
54
  const meta = ' ' + c.dim('agentic terminal') + ' ' + diamond + ' ' + tag;
55
55
  console.log('\n' + wordmark);
56
+ console.log('');
56
57
  console.log(meta + '\n');
57
58
  }
58
59