zames_pro 1.1.1 → 1.2.1

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/src/input.js ADDED
@@ -0,0 +1,447 @@
1
+ import { theme } from './theme.js'
2
+ import { renderMarkdown } from './markdown.js'
3
+ import { randomThinkingPhrase, stripEllipsis } from './spinner.js'
4
+
5
+ // Постоянная строка ввода внизу терминала + область статуса/вывода над ней.
6
+ //
7
+ // Зачем свой редактор:
8
+ // * строка ввода видна ВСЕГДА и не затирается выводом агента;
9
+ // * спиннер/статус и ответы печатаются ВЫШЕ строки ввода (printAbove);
10
+ // * перерисовка учитывает перенос по ширине терминала, поэтому нет
11
+ // дублирования строк при многострочном вводе;
12
+ // * многострочный ввод: Enter отправляет, Ctrl+J (и Shift+Enter в
13
+ // терминалах с расширенным протоколом) вставляют перевод строки.
14
+ //
15
+ // Все управляющие символы собираются из кодов, чтобы в файле не было
16
+ // «сырых» ESC/CR/LF в строковых литералах (см. AGENTS.md).
17
+
18
+ const NL = String.fromCharCode(10)
19
+ const CR = String.fromCharCode(13)
20
+ const ESC = String.fromCharCode(27)
21
+ const PASTE_START = ESC + '[200~'
22
+ const PASTE_END = ESC + '[201~'
23
+
24
+ const DOTS = ['.', '..', '...']
25
+ const HINT = theme.dim(' · Enter — отправить, Ctrl+J — новая строка, Esc — стоп')
26
+
27
+ function safeJson(v) {
28
+ try {
29
+ return JSON.stringify(v)
30
+ } catch {
31
+ return String(v)
32
+ }
33
+ }
34
+
35
+ // Видимая длина строки без ANSI-последовательностей.
36
+ export function visLen(s) {
37
+ s = String(s)
38
+ let n = 0
39
+ let i = 0
40
+ while (i < s.length) {
41
+ if (s.charCodeAt(i) === 27) {
42
+ i++
43
+ if (s[i] === '[') {
44
+ i++
45
+ while (i < s.length && !/[A-Za-z]/.test(s[i])) i++
46
+ i++
47
+ }
48
+ continue
49
+ }
50
+ n++
51
+ i++
52
+ }
53
+ return n
54
+ }
55
+
56
+ // Раскладка ввода на визуальные строки с учётом ширины терминала.
57
+ // Возвращает строки (с префиксом) и позицию курсора в визуальных координатах.
58
+ export function layoutInput(promptStr, buf, cursor, cols) {
59
+ const width = Math.max(20, Number(cols) || 80)
60
+ const promptW = visLen(promptStr)
61
+ const pad = ' '.repeat(promptW)
62
+ const chars = Array.from(String(buf))
63
+ const cur = Math.max(0, Math.min(Number(cursor) || 0, chars.length))
64
+ const rows = []
65
+ let i = 0
66
+ while (true) {
67
+ const first = rows.length === 0
68
+ const prefix = first ? promptStr : pad
69
+ const avail = Math.max(1, width - promptW)
70
+ let text = ''
71
+ let count = 0
72
+ const start = i
73
+ while (i < chars.length && chars[i] !== NL && count < avail) {
74
+ text += chars[i]
75
+ i++
76
+ count++
77
+ }
78
+ if (i < chars.length && chars[i] === NL) {
79
+ i++
80
+ rows.push({ prefix, text, start })
81
+ continue
82
+ }
83
+ rows.push({ prefix, text, start })
84
+ if (i >= chars.length) break
85
+ }
86
+
87
+ let cursorRow = 0
88
+ let cursorCol = promptW
89
+ for (let r = 0; r < rows.length; r++) {
90
+ const row = rows[r]
91
+ const len = Array.from(row.text).length
92
+ const from = row.start
93
+ const to = row.start + len
94
+ if (cur >= from && cur <= to) {
95
+ const next = rows[r + 1]
96
+ if (cur === to && next && next.start === to) continue
97
+ cursorRow = r
98
+ cursorCol = promptW + (cur - from)
99
+ break
100
+ }
101
+ }
102
+ return { rows, cursorRow, cursorCol }
103
+ }
104
+
105
+ export class LineEditor {
106
+ constructor({ prompt = '> ' } = {}) {
107
+ this.promptStr = prompt
108
+ this.buf = ''
109
+ this.cursor = 0
110
+ this.statusText = ''
111
+ this.pendingText = null
112
+ this.rendered = false
113
+ this.cursorRowFromTop = 0
114
+ this.busy = false
115
+ this.onSubmit = null
116
+ this.onEscape = null
117
+ this.onCtrlC = null
118
+ this._inPaste = false
119
+ this._dotTimer = null
120
+ this._dotPhase = 0
121
+ this._thinkBase = ''
122
+ this._wasRaw = false
123
+ this._onData = (b) => this._handle(b)
124
+ }
125
+
126
+ start() {
127
+ const stdin = process.stdin
128
+ if (!stdin.isTTY || !stdin.setRawMode) return false
129
+ this._wasRaw = stdin.isRaw
130
+ stdin.setRawMode(true)
131
+ stdin.resume()
132
+ process.stdout.write(ESC + '[?2004h')
133
+ stdin.on('data', this._onData)
134
+ this._writeBlock()
135
+ return true
136
+ }
137
+
138
+ dispose() {
139
+ const stdin = process.stdin
140
+ stdin.removeListener('data', this._onData)
141
+ process.stdout.write(ESC + '[?2004l')
142
+ this._stopDots()
143
+ if (this.rendered) this._eraseBlock()
144
+ if (stdin.setRawMode) stdin.setRawMode(this._wasRaw || false)
145
+ }
146
+
147
+ setPrompt(str) {
148
+ this.promptStr = str
149
+ this._render()
150
+ }
151
+
152
+ clear() {
153
+ this.buf = ''
154
+ this.cursor = 0
155
+ this._render()
156
+ }
157
+
158
+ // ---------- отрисовка ----------
159
+
160
+ _eraseBlock() {
161
+ if (!this.rendered) return
162
+ if (this.cursorRowFromTop > 0) {
163
+ process.stdout.write(ESC + '[' + this.cursorRowFromTop + 'A')
164
+ }
165
+ process.stdout.write(CR + ESC + '[J')
166
+ this.rendered = false
167
+ }
168
+
169
+ _writeBlock() {
170
+ const cols = process.stdout.columns || 80
171
+ let out = ''
172
+ let top = 0
173
+ if (this.statusText) {
174
+ out += this.statusText + NL
175
+ top = 1
176
+ }
177
+ const lay = layoutInput(this.promptStr, this.buf, this.cursor, cols)
178
+ out += lay.rows.map((r) => r.prefix + r.text).join(NL)
179
+ process.stdout.write(out)
180
+ const lastRow = lay.rows.length - 1
181
+ const up = lastRow - lay.cursorRow
182
+ if (up > 0) process.stdout.write(ESC + '[' + up + 'A')
183
+ process.stdout.write(CR)
184
+ if (lay.cursorCol > 0) process.stdout.write(ESC + '[' + lay.cursorCol + 'C')
185
+ this.rendered = true
186
+ this.cursorRowFromTop = top + lay.cursorRow
187
+ }
188
+
189
+ _render() {
190
+ this._eraseBlock()
191
+ this._writeBlock()
192
+ }
193
+
194
+ // Напечатать блок ВЫШЕ строки ввода и вернуть строку ввода на место.
195
+ printAbove(text) {
196
+ this._eraseBlock()
197
+ process.stdout.write(String(text) + NL)
198
+ this._writeBlock()
199
+ }
200
+
201
+ // ---------- интерфейс, совместимый со спиннером ----------
202
+
203
+ setStatus(text) {
204
+ this.statusText = text || ''
205
+ this._render()
206
+ }
207
+
208
+ _stopDots() {
209
+ if (this._dotTimer) {
210
+ clearInterval(this._dotTimer)
211
+ this._dotTimer = null
212
+ }
213
+ }
214
+
215
+ _startThinking() {
216
+ if (this.pendingText) {
217
+ this.setStatus(theme.prompt('✎ ') + this.pendingText + HINT)
218
+ return
219
+ }
220
+ this._thinkBase = theme.brown(stripEllipsis(randomThinkingPhrase()))
221
+ this._dotPhase = 0
222
+ this.setStatus(this._thinkBase + DOTS[0] + HINT)
223
+ this._stopDots()
224
+ this._dotTimer = setInterval(() => {
225
+ this._dotPhase = (this._dotPhase + 1) % DOTS.length
226
+ this.setStatus(this._thinkBase + DOTS[this._dotPhase] + HINT)
227
+ }, 400)
228
+ if (this._dotTimer.unref) this._dotTimer.unref()
229
+ }
230
+
231
+ thinking() {
232
+ this._startThinking()
233
+ }
234
+
235
+ setPending(text) {
236
+ this.pendingText = text && String(text).length ? String(text) : null
237
+ if (this.pendingText) {
238
+ this._stopDots()
239
+ this.setStatus(theme.prompt('✎ ') + this.pendingText + HINT)
240
+ } else {
241
+ this._startThinking()
242
+ }
243
+ }
244
+
245
+ stop() {
246
+ this._stopDots()
247
+ this.setStatus('')
248
+ }
249
+
250
+ toolCall(name, args) {
251
+ const preview = safeJson(args).slice(0, 120)
252
+ this.printAbove(theme.tool('🔧 ' + name) + ' ' + theme.dim(preview))
253
+ }
254
+
255
+ toolResult(result) {
256
+ const text = typeof result === 'string' ? result : safeJson(result)
257
+ const preview = text.slice(0, 200).split(NL).join(' ↵ ')
258
+ this.printAbove(theme.toolResult(' → ' + preview))
259
+ }
260
+
261
+ assistant(msg) {
262
+ this.stop()
263
+ const rendered = renderMarkdown(msg)
264
+ this.printAbove(
265
+ NL + theme.assistant('● Ответ') + NL + rendered + NL + theme.dim('─'.repeat(60)),
266
+ )
267
+ }
268
+
269
+ // ---------- ввод ----------
270
+
271
+ _submit() {
272
+ const text = this.buf
273
+ if (!text.trim()) {
274
+ this._render()
275
+ return
276
+ }
277
+ this.buf = ''
278
+ this.cursor = 0
279
+ this.pendingText = null
280
+ this._stopDots()
281
+ this.statusText = ''
282
+ this.printAbove(theme.user('❯ ') + text)
283
+ if (this.onSubmit) this.onSubmit(text)
284
+ }
285
+
286
+ _insert(text) {
287
+ const chars = Array.from(this.buf)
288
+ const ins = Array.from(String(text))
289
+ const next = chars.slice(0, this.cursor).concat(ins, chars.slice(this.cursor))
290
+ this.buf = next.join('')
291
+ this.cursor += ins.length
292
+ }
293
+
294
+ _backspace() {
295
+ if (this.cursor <= 0) return
296
+ const chars = Array.from(this.buf)
297
+ chars.splice(this.cursor - 1, 1)
298
+ this.buf = chars.join('')
299
+ this.cursor--
300
+ }
301
+
302
+ _delete() {
303
+ const chars = Array.from(this.buf)
304
+ if (this.cursor >= chars.length) return
305
+ chars.splice(this.cursor, 1)
306
+ this.buf = chars.join('')
307
+ }
308
+
309
+ _left() {
310
+ if (this.cursor > 0) this.cursor--
311
+ }
312
+
313
+ _right() {
314
+ if (this.cursor < Array.from(this.buf).length) this.cursor++
315
+ }
316
+
317
+ _home() {
318
+ const chars = Array.from(this.buf)
319
+ let i = this.cursor
320
+ while (i > 0 && chars[i - 1] !== NL) i--
321
+ this.cursor = i
322
+ }
323
+
324
+ _end() {
325
+ const chars = Array.from(this.buf)
326
+ let i = this.cursor
327
+ while (i < chars.length && chars[i] !== NL) i++
328
+ this.cursor = i
329
+ }
330
+
331
+ _up() {
332
+ const chars = Array.from(this.buf)
333
+ let start = this.cursor
334
+ while (start > 0 && chars[start - 1] !== NL) start--
335
+ if (start === 0) return
336
+ const col = this.cursor - start
337
+ const prevEnd = start - 1
338
+ let prevStart = prevEnd
339
+ while (prevStart > 0 && chars[prevStart - 1] !== NL) prevStart--
340
+ this.cursor = prevStart + Math.min(col, prevEnd - prevStart)
341
+ }
342
+
343
+ _down() {
344
+ const chars = Array.from(this.buf)
345
+ let end = this.cursor
346
+ while (end < chars.length && chars[end] !== NL) end++
347
+ if (end >= chars.length) return
348
+ let lineStart = this.cursor
349
+ while (lineStart > 0 && chars[lineStart - 1] !== NL) lineStart--
350
+ const col = this.cursor - lineStart
351
+ const nextStart = end + 1
352
+ let nextEnd = nextStart
353
+ while (nextEnd < chars.length && chars[nextEnd] !== NL) nextEnd++
354
+ this.cursor = nextStart + Math.min(col, nextEnd - nextStart)
355
+ }
356
+
357
+ _handle(data) {
358
+ let s = data.toString('utf-8')
359
+
360
+ if (!this._inPaste && s === ESC) {
361
+ if (this.onEscape) this.onEscape()
362
+ return
363
+ }
364
+
365
+ while (s.length) {
366
+ if (this._inPaste) {
367
+ const end = s.indexOf(PASTE_END)
368
+ if (end === -1) {
369
+ this._insert(s)
370
+ s = ''
371
+ } else {
372
+ this._insert(s.slice(0, end))
373
+ s = s.slice(end + PASTE_END.length)
374
+ this._inPaste = false
375
+ }
376
+ this._render()
377
+ continue
378
+ }
379
+
380
+ const pasteAt = s.indexOf(PASTE_START)
381
+ if (pasteAt !== -1) {
382
+ const before = s.slice(0, pasteAt)
383
+ s = s.slice(pasteAt + PASTE_START.length)
384
+ this._inPaste = true
385
+ if (before) {
386
+ this._insert(before)
387
+ this._render()
388
+ }
389
+ continue
390
+ }
391
+
392
+ // Shift+Enter в терминалах с расширенным протоколом и Alt+Enter.
393
+ if (s.startsWith(ESC + '[13;2u')) { this._insert(NL); s = s.slice(7); this._render(); continue }
394
+ if (s.startsWith(ESC + '[27;2;13~')) { this._insert(NL); s = s.slice(10); this._render(); continue }
395
+ if (s.startsWith(ESC + NL)) { this._insert(NL); s = s.slice(2); this._render(); continue }
396
+
397
+ const ch = s[0]
398
+ const code = s.charCodeAt(0)
399
+ s = s.slice(1)
400
+
401
+ if (ch === CR) { this._submit(); continue }
402
+ if (code === 10) { this._insert(NL); this._render(); continue }
403
+ if (code === 3) { if (this.onCtrlC) this.onCtrlC(); continue }
404
+ if (code === 4) { if (!this.buf && this.onCtrlC) this.onCtrlC(); continue }
405
+ if (code === 1) { this._home(); this._render(); continue }
406
+ if (code === 5) { this._end(); this._render(); continue }
407
+ if (code === 21) { this.buf = ''; this.cursor = 0; this._render(); continue }
408
+ if (code === 127 || code === 8) { this._backspace(); this._render(); continue }
409
+
410
+ if (code === 27) {
411
+ if (s.startsWith('[D')) { this._left(); s = s.slice(2); this._render(); continue }
412
+ if (s.startsWith('[C')) { this._right(); s = s.slice(2); this._render(); continue }
413
+ if (s.startsWith('[A')) { this._up(); s = s.slice(2); this._render(); continue }
414
+ if (s.startsWith('[B')) { this._down(); s = s.slice(2); this._render(); continue }
415
+ if (s.startsWith('[H') || s.startsWith('[1~')) { this._home(); s = s.slice(s.startsWith('[1~') ? 3 : 2); this._render(); continue }
416
+ if (s.startsWith('[F') || s.startsWith('[4~')) { this._end(); s = s.slice(s.startsWith('[4~') ? 3 : 2); this._render(); continue }
417
+ if (s.startsWith('[3~')) { this._delete(); s = s.slice(3); this._render(); continue }
418
+ let j = 0
419
+ while (j < s.length && !/[A-Za-z~]/.test(s[j])) j++
420
+ s = s.slice(j + 1)
421
+ continue
422
+ }
423
+
424
+ if (code < 32) continue
425
+ this._insert(ch)
426
+ this._render()
427
+ }
428
+ }
429
+ }
430
+
431
+ // Проверка раскладки: node src/input.js --selftest
432
+ export function selftest() {
433
+ const cases = [
434
+ { p: '> ', b: 'hello', c: 5, w: 80 },
435
+ { p: '> ', b: 'a' + NL + 'b', c: 3, w: 80 },
436
+ { p: '> ', b: 'x'.repeat(200), c: 200, w: 40 },
437
+ { p: '> ', b: '', c: 0, w: 40 },
438
+ ]
439
+ for (const t of cases) {
440
+ const r = layoutInput(t.p, t.b, t.c, t.w)
441
+ console.log(JSON.stringify({ rows: r.rows.length, row: r.cursorRow, col: r.cursorCol }))
442
+ }
443
+ }
444
+
445
+ if (process.argv[1] && process.argv[1].endsWith('input.js') && process.argv.includes('--selftest')) {
446
+ selftest()
447
+ }
@@ -0,0 +1,110 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import os from 'os'
4
+
5
+ // Сессии (чаты DeepSeek) храним в отдельной папке, чтобы они не терялись
6
+ // после перезапуска процесса. Каждая сессия — отдельный JSON-файл
7
+ // <id>.json в ~/.zames/.sessions. last.json указывает на последнюю
8
+ // открытую сессию для конкретной рабочей директории.
9
+ const SESSIONS_DIR = path.join(os.homedir(), '.zames', '.sessions')
10
+ const INDEX_FILE = path.join(SESSIONS_DIR, 'last.json')
11
+
12
+ function ensureDir() {
13
+ fs.mkdirSync(SESSIONS_DIR, { recursive: true })
14
+ }
15
+
16
+ function safeName(id) {
17
+ return String(id).replace(/[^a-zA-Z0-9_.-]/g, '_')
18
+ }
19
+
20
+ // Сохраняем/обновляем сессию. workdir нужен, чтобы при запуске из того же
21
+ // проекта восстанавливать именно его последний чат.
22
+ export function saveSession({ id, title = '', workdir = '' }) {
23
+ if (!id) return
24
+ try {
25
+ ensureDir()
26
+ const file = path.join(SESSIONS_DIR, safeName(id) + '.json')
27
+ let prev = {}
28
+ try { prev = JSON.parse(fs.readFileSync(file, 'utf-8')) } catch {}
29
+ const now = new Date().toISOString()
30
+ const data = {
31
+ id,
32
+ title: title || prev.title || '',
33
+ workdir: workdir || prev.workdir || '',
34
+ createdAt: prev.createdAt || now,
35
+ updatedAt: now,
36
+ }
37
+ fs.writeFileSync(file, JSON.stringify(data, null, 2), 'utf-8')
38
+ writeIndex({ id, workdir: data.workdir })
39
+ return data
40
+ } catch {
41
+ return null
42
+ }
43
+ }
44
+
45
+ function writeIndex({ id, workdir }) {
46
+ try {
47
+ ensureDir()
48
+ let index = {}
49
+ try { index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8')) } catch {}
50
+ if (!index || typeof index !== 'object') index = {}
51
+ if (!index.byWorkdir || typeof index.byWorkdir !== 'object') index.byWorkdir = {}
52
+ const key = workdir || ''
53
+ index.byWorkdir[key] = id
54
+ index.last = id
55
+ index.updatedAt = new Date().toISOString()
56
+ fs.writeFileSync(INDEX_FILE, JSON.stringify(index, null, 2), 'utf-8')
57
+ } catch {}
58
+ }
59
+
60
+ // Последняя сессия для рабочей директории. Если для неё ничего нет —
61
+ // возвращаем последнюю сессию вообще (полезно при запуске из нового места).
62
+ export function loadLastSession(workdir = '') {
63
+ try {
64
+ const index = JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8'))
65
+ const byWorkdir = index && index.byWorkdir ? index.byWorkdir : {}
66
+ const candidates = [byWorkdir[workdir], index.last].filter(Boolean)
67
+ for (const id of candidates) {
68
+ // Файл сессии мог быть удалён — индекс тогда устарел, и сессию
69
+ // восстанавливать нельзя. Пробуем следующий кандидат.
70
+ const s = readSession(id)
71
+ if (s) return s
72
+ }
73
+ return null
74
+ } catch {
75
+ return null
76
+ }
77
+ }
78
+
79
+ export function readSession(id) {
80
+ if (!id) return null
81
+ try {
82
+ const file = path.join(SESSIONS_DIR, safeName(id) + '.json')
83
+ return JSON.parse(fs.readFileSync(file, 'utf-8'))
84
+ } catch {
85
+ return null
86
+ }
87
+ }
88
+
89
+ // Список всех сессий, свежие — первыми.
90
+ export function listSessions() {
91
+ try {
92
+ ensureDir()
93
+ const files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.json') && f !== 'last.json')
94
+ const out = []
95
+ for (const f of files) {
96
+ try {
97
+ const data = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf-8'))
98
+ if (data && data.id) out.push(data)
99
+ } catch {}
100
+ }
101
+ out.sort((a, b) => String(b.updatedAt || '').localeCompare(String(a.updatedAt || '')))
102
+ return out
103
+ } catch {
104
+ return []
105
+ }
106
+ }
107
+
108
+ export function sessionsDir() {
109
+ return SESSIONS_DIR
110
+ }
package/src/spinner.js CHANGED
@@ -36,12 +36,22 @@ const THINKING_PHRASES = [
36
36
  'Замешиваю кал из всего подряд…',
37
37
  ]
38
38
 
39
- function randomThinkingPhrase() {
39
+ export function randomThinkingPhrase() {
40
40
  return THINKING_PHRASES[Math.floor(Math.random() * THINKING_PHRASES.length)]
41
41
  }
42
42
 
43
+ // Убираем завершающее многоточие из фразы — точки анимируем отдельно.
44
+ export function stripEllipsis(phrase) {
45
+ return phrase.replace(/[.…]+\s*$/, '')
46
+ }
47
+
43
48
  export function createSpinner() {
44
49
  let spinner = null
50
+ let dotTimer = null
51
+ let dotPhase = 0
52
+ let pending = null
53
+
54
+ const DOTS = ['.', '..', '...']
45
55
 
46
56
  const start = (text) => {
47
57
  if (!spinner) spinner = ora(text).start()
@@ -49,11 +59,59 @@ export function createSpinner() {
49
59
  }
50
60
 
51
61
  const stop = () => {
62
+ if (dotTimer) {
63
+ clearInterval(dotTimer)
64
+ dotTimer = null
65
+ }
52
66
  if (spinner) spinner.stop()
53
67
  }
54
68
 
69
+ // Подсказка в строке статуса: во время работы агента терминал живой,
70
+ // можно печатать следующее сообщение. Без неё это неочевидно.
71
+ const HINT = theme.dim(' · печатай — Enter в очередь, Esc стоп')
72
+
73
+ // Запуск анимированного статуса: коричневый текст + «бегущие» точки.
74
+ const startThinking = () => {
75
+ const base = theme.brown(stripEllipsis(randomThinkingPhrase()))
76
+ start(base + DOTS[0] + HINT)
77
+ dotPhase = 0
78
+ if (dotTimer) clearInterval(dotTimer)
79
+ dotTimer = setInterval(() => {
80
+ if (!spinner) return
81
+ dotPhase = (dotPhase + 1) % DOTS.length
82
+ spinner.text = base + DOTS[dotPhase] + HINT
83
+ }, 400)
84
+ if (dotTimer.unref) dotTimer.unref()
85
+ }
86
+
87
+ // Показать набранный, но ещё не отправленный текст вместо спиннера.
88
+ // Позволяет печатать сообщение прямо во время работы агента.
89
+ const showPending = () => {
90
+ if (dotTimer) {
91
+ clearInterval(dotTimer)
92
+ dotTimer = null
93
+ }
94
+ if (!spinner) spinner = ora('')
95
+ spinner.start()
96
+ spinner.text = theme.prompt('✎ ') + pending + HINT
97
+ }
98
+
55
99
  return {
56
- thinking: () => start(theme.system(randomThinkingPhrase())),
100
+ thinking: () => {
101
+ if (pending) {
102
+ showPending()
103
+ return
104
+ }
105
+ startThinking()
106
+ },
107
+
108
+ // Текст, который пользователь набирает во время работы агента.
109
+ // Пустая строка / null — вернуть обычный спиннер.
110
+ setPending: (text) => {
111
+ pending = text && String(text).length ? String(text) : null
112
+ if (pending) showPending()
113
+ else startThinking()
114
+ },
57
115
 
58
116
  toolCall: (name, args) => {
59
117
  stop()