@itcase/config 1.6.79 → 1.6.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -90,8 +90,10 @@ npm install --save-dev commitizen @commitlint/cz-commitlint
90
90
  `smart-commit` — Cursor Agent (`ask`) планирует коммиты по `SCOPES`, печатает сообщения, спрашивает `[y/N]`. Перед коммитом ждёт и снимает устаревший `.git/index.lock`, если его никто не держит. Lockfile (`package-lock.json` и аналоги) всегда кладётся в тот же коммит, что и соседний `package.json`; при одном только bump deps агент не вызывается.
91
91
 
92
92
  ```text
93
- [yellow pixel banana animation]
93
+ [random pixel animation]
94
94
  Drafting commit messages… (12s)
95
+ Agent started (Composer)
96
+ Reading src/foo.ts
95
97
 
96
98
  Commits:
97
99
  • feat(other): add smart-commit cli
@@ -146,7 +148,7 @@ CLI ставится в `~/.local/bin`. Логин в Cursor IDE этот CLI н
146
148
 
147
149
  Обновление CLI: `agent update`.
148
150
 
149
- Пока Agent думает, в терминале крутится pixel-анимация (случайно nyan cat или dancing banana) + статус `(Ns)` на одной области экрана (`log-update`). Принудительно: `--cat` / `--banana`. Для полного лога: `npm run smart-commit -- --verbose --dry-run`.
151
+ Пока Agent думает, в терминале крутится pixel-анимация (случайно nyan cat, dancing banana, упячка или Pac-Man) + статус `(Ns)`, а под ними живой лог агента (`log-update`). Пока процесс агента жив, CLI его не убивает по таймауту. Принудительно: `--cat` / `--banana` / `--upyachka` / `--pacman`. Для полного лога: `npm run smart-commit -- --verbose --dry-run`.
150
152
 
151
153
  > **Важно:** в npm есть одноимённый пакет `smart-commit`. Используйте `npm run smart-commit` из проекта с `@itcase/config` — bin берётся из `node_modules`. `npx smart-commit` без локальной установки может подтянуть чужой пакет; CLI перед запуском проверяет, что выполняется именно `@itcase/config`.
152
154
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itcase/config",
3
- "version": "1.6.79",
3
+ "version": "1.6.81",
4
4
  "author": "ITCase",
5
5
  "description": "ITCase Config",
6
6
  "engines": {
@@ -63,23 +63,23 @@
63
63
  "@svgr/plugin-jsx": "^8.1.0",
64
64
  "@svgr/plugin-svgo": "^8.1.0",
65
65
  "@svgr/webpack": "^8.1.0",
66
- "autoprefixer": "^10.5.4",
66
+ "autoprefixer": "^10.5.5",
67
67
  "chokidar-cli": "^3.0.0",
68
68
  "commitizen": "^4.3.2",
69
69
  "conventional-changelog-conventionalcommits": "9.3.1",
70
- "cssnano": "^9.0.2",
71
- "cssnano-preset-default": "^9.0.2",
72
- "inquirer": "^14.2.0",
70
+ "cssnano": "^9.0.4",
71
+ "cssnano-preset-default": "^9.0.4",
72
+ "inquirer": "^14.2.2",
73
73
  "log-update": "^8.0.0",
74
- "postcss": "^8.5.26",
74
+ "postcss": "^8.5.28",
75
75
  "postcss-aspect-ratio-polyfill": "^2.0.0",
76
76
  "postcss-clamp": "^4.1.0",
77
- "postcss-cli": "^11.0.1",
77
+ "postcss-cli": "^12.0.0",
78
78
  "postcss-color-hsla-fallback": "^1.0.0",
79
79
  "postcss-combine-duplicated-selectors": "^10.0.3",
80
80
  "postcss-convert-unit": "^0.1.3",
81
81
  "postcss-dark-theme-class": "^2.0.0",
82
- "postcss-discard-duplicates": "^9.0.1",
82
+ "postcss-discard-duplicates": "^9.0.2",
83
83
  "postcss-each": "^1.1.0",
84
84
  "postcss-easings": "^4.0.0",
85
85
  "postcss-extend-rule": "^4.0.0",
@@ -95,7 +95,7 @@
95
95
  "postcss-nested-ancestors": "^3.0.0",
96
96
  "postcss-normalize": "^13.0.1",
97
97
  "postcss-prepend-imports": "^1.0.1",
98
- "postcss-preset-env": "^11.5.1",
98
+ "postcss-preset-env": "^11.5.2",
99
99
  "postcss-pxtorem": "^6.1.0",
100
100
  "postcss-sort-media-queries": "^6.7.1",
101
101
  "postcss-unit": "^0.0.3",
@@ -109,17 +109,17 @@
109
109
  "rollup-plugin-peer-deps-external": "^2.2.4",
110
110
  "rollup-preserve-directives": "^1.1.3",
111
111
  "semantic-release": "^25.0.9",
112
- "semantic-release-lerna": "^3.6.3"
112
+ "semantic-release-lerna": "^3.6.4"
113
113
  },
114
114
  "devDependencies": {
115
- "@itcase/lint": "^1.1.122",
115
+ "@itcase/lint": "^1.1.123",
116
116
  "eslint": "9.39.2",
117
117
  "husky": "^9.1.7",
118
- "inquirer": "^14.2.0",
119
- "lint-staged": "^17.4.1",
118
+ "inquirer": "^14.2.2",
119
+ "lint-staged": "^17.5.0",
120
120
  "prettier": "^3.9.6",
121
- "storybook": "^10.5.10",
122
- "stylelint": "^17.14.1",
121
+ "storybook": "^10.6.0",
122
+ "stylelint": "^17.15.0",
123
123
  "typescript": "^6.0.3"
124
124
  }
125
125
  }
@@ -17,6 +17,8 @@ import {
17
17
  } from '../commitlint/commitlintCommon.js'
18
18
  import { getBananaFrame } from './dancing-banana.js'
19
19
  import { getNyanFrame } from './nyan-cat.js'
20
+ import { getPacmanFrame } from './pacman.js'
21
+ import { getUpyachkaFrame } from './upyachka.js'
20
22
 
21
23
  const PACKAGE_NAME = '@itcase/config'
22
24
  const CLI_RELATIVE = 'smart-commit/cli.js'
@@ -30,10 +32,14 @@ const LOCKFILE_NAMES = new Set([
30
32
  'bun.lock',
31
33
  'bun.lockb',
32
34
  ])
33
- const AGENT_TIMEOUT_MS = 120_000
34
35
  const AGENT_STEP_MS = 2_500
36
+ const AGENT_STILL_WORKING_S = 120
37
+ const AGENT_LOG_ROWS = 8
38
+ const AGENT_LOG_MAX = 160
35
39
  const NYAN_FRAME_MS = 90
36
40
  const BANANA_FRAME_MS = 100
41
+ const PACMAN_FRAME_MS = 120
42
+ const UPYACHKA_FRAME_MS = 100
37
43
  const INDEX_LOCK_RETRIES = 8
38
44
  const INDEX_LOCK_WAIT_MS = 250
39
45
  const STALE_LOCK_MS = 1500
@@ -43,13 +49,14 @@ const AGENT_WAIT_STEPS = [
43
49
  'Waiting for response',
44
50
  ]
45
51
  const HELP = `smart-commit [--dry-run] [--yes|-y] [--type t] [--model id] [--verbose]
46
- [--cat|--nyan] [--banana]
52
+ [--cat|--nyan] [--banana] [--upyachka] [--pacman]
47
53
 
48
54
  Cursor Agent → commit messages by SCOPES → [y/N] → git commit
49
55
  Run via: npm exec smart-commit (from project with @itcase/config)
50
56
  Needs: agent login (or CURSOR_API_KEY) + commitlint.config.* with SCOPES
51
57
 
52
- Progress sprite is random (nyan cat or banana). Force with --cat or --banana.
58
+ Progress sprite is random (nyan cat, banana, upyachka, or Pac-Man).
59
+ Force with --cat, --banana, --upyachka, or --pacman. Agent log is below.
53
60
  `
54
61
  const AGENT_MISSING = `Cursor Agent CLI not found — smart-commit cannot run.
55
62
 
@@ -142,14 +149,25 @@ async function main(argv) {
142
149
 
143
150
  ensureGitIndexReady(cwd)
144
151
 
152
+ const total = activePlans.length
153
+ process.stdout.write(`Creating ${total} commit(s)…\n`)
154
+
145
155
  let created = 0
146
- for (const plan of activePlans) {
156
+ for (let index = 0; index < total; index++) {
157
+ const plan = activePlans[index]
158
+ const step = `[${index + 1}/${total}]`
147
159
  const planFiles = plan.files.filter((file) => fileHasChanges(cwd, file))
148
160
  if (!planFiles.length) {
149
- process.stdout.write(`Skipping empty commit: ${plan.message}\n`)
161
+ process.stdout.write(`${step} skip empty: ${plan.message}\n`)
150
162
  continue
151
163
  }
152
164
 
165
+ const startedAt = Date.now()
166
+ const fileLabel =
167
+ planFiles.length === 1 ? '1 file' : `${planFiles.length} files`
168
+ process.stdout.write(`${step} ${plan.message}\n`)
169
+ process.stdout.write(` staging ${fileLabel}…\n`)
170
+
153
171
  // Drop leftover index entries so only this plan's files are committed.
154
172
  git(cwd, ['reset', '-q', 'HEAD'])
155
173
  stageFiles(cwd, planFiles)
@@ -162,6 +180,7 @@ async function main(argv) {
162
180
  )
163
181
  }
164
182
 
183
+ process.stdout.write(' git commit (husky)…\n')
165
184
  try {
166
185
  git(cwd, ['commit', '-m', plan.message], { inherit: true })
167
186
  } catch (error) {
@@ -170,6 +189,7 @@ async function main(argv) {
170
189
  })
171
190
  }
172
191
  created++
192
+ process.stdout.write(` done (${formatElapsed(startedAt)})\n`)
173
193
  }
174
194
  process.stdout.write(`Created ${created} commit(s).\n`)
175
195
 
@@ -200,6 +220,8 @@ function parseArgs(argv) {
200
220
  else if (arg === '--staged') option.staged = true
201
221
  else if (arg === '--cat' || arg === '--nyan') option.sprite = 'cat'
202
222
  else if (arg === '--banana') option.sprite = 'banana'
223
+ else if (arg === '--upyachka') option.sprite = 'upyachka'
224
+ else if (arg === '--pacman') option.sprite = 'pacman'
203
225
  else if (arg === '--type') option.type = argv[++i] || ''
204
226
  else if (arg === '--model') option.model = argv[++i] || ''
205
227
  else if (arg.startsWith('--type=')) option.type = arg.slice(7)
@@ -210,8 +232,9 @@ function parseArgs(argv) {
210
232
  }
211
233
 
212
234
  function pickSprite(forced) {
213
- if (forced === 'cat' || forced === 'banana') return forced
214
- return Math.random() < 0.5 ? 'cat' : 'banana'
235
+ const sprites = ['cat', 'banana', 'upyachka', 'pacman']
236
+ if (sprites.includes(forced)) return forced
237
+ return sprites[Math.floor(Math.random() * sprites.length)]
215
238
  }
216
239
 
217
240
  function assertItcaseConfigPackage(cwd) {
@@ -447,49 +470,109 @@ async function withAgentProgress(context, callback) {
447
470
  let phase = 'early'
448
471
  let receivedChars = 0
449
472
  let frameIndex = 0
473
+ const logLines = []
474
+ const streamByKey = { assistant: -1, thinking: -1 }
475
+
476
+ const elapsedSeconds = () => Math.floor((Date.now() - startedAt) / 1000)
450
477
 
451
- const onAgentActivity = (charCount) => {
452
- if (charCount > 0) {
453
- receivedChars = charCount
478
+ const onAgentActivity = (activity = {}) => {
479
+ if (activity.charCount > 0) {
480
+ receivedChars = activity.charCount
454
481
  phase = 'receiving'
455
482
  }
483
+ if (!activity.log) return
484
+ if (activity.stream) {
485
+ const key = activity.streamKey || 'assistant'
486
+ const index = streamByKey[key]
487
+ if (index >= 0 && index < logLines.length) logLines[index] = activity.log
488
+ else {
489
+ logLines.push(activity.log)
490
+ streamByKey[key] = logLines.length - 1
491
+ }
492
+ } else if (logLines[logLines.length - 1] !== activity.log) {
493
+ logLines.push(activity.log)
494
+ streamByKey.assistant = -1
495
+ }
496
+ if (phase !== 'receiving') phase = 'working'
456
497
  }
457
498
 
458
499
  const currentLabel = () => {
500
+ const elapsed = elapsedSeconds()
459
501
  if (phase === 'receiving') {
460
502
  return `Receiving response (${receivedChars} chars)`
461
503
  }
462
- if (phase === 'waiting') {
504
+ if (elapsed >= AGENT_STILL_WORKING_S) return 'Agent still working'
505
+ if (phase === 'waiting' || phase === 'working') {
463
506
  return AGENT_WAIT_STEPS[waitStepIndex % AGENT_WAIT_STEPS.length]
464
507
  }
465
508
  return earlySteps[Math.min(earlyStepIndex, earlySteps.length - 1)]
466
509
  }
467
510
 
468
511
  const formatText = () => {
469
- const elapsed = Math.floor((Date.now() - startedAt) / 1000)
470
- return `${currentLabel()}… (${elapsed}s)`
512
+ const text = `${currentLabel()}… (${elapsedSeconds()}s)`
513
+ const width = process.stderr.columns || process.stdout.columns || 80
514
+ return clipLog(text, width)
515
+ }
516
+
517
+ const reportActivity = (activity) => {
518
+ onAgentActivity(activity)
519
+ if (
520
+ (!process.stderr.isTTY || context.verbose) &&
521
+ activity.log &&
522
+ !activity.stream
523
+ ) {
524
+ process.stderr.write(`${activity.log}\n`)
525
+ }
471
526
  }
472
527
 
473
528
  if (!process.stderr.isTTY || context.verbose) {
474
529
  process.stderr.write(`${formatText()}\n`)
475
- return callback({ onAgentActivity })
530
+ const waitTimer = setInterval(() => {
531
+ if (phase === 'receiving') return
532
+ process.stderr.write(`Thinking… (${elapsedSeconds()}s)\n`)
533
+ }, AGENT_STEP_MS)
534
+ try {
535
+ return await callback({ onAgentActivity: reportActivity })
536
+ } finally {
537
+ clearInterval(waitTimer)
538
+ }
476
539
  }
477
540
 
478
541
  const columns = process.stderr.columns || process.stdout.columns || 80
479
542
  const rows = process.stderr.rows || process.stdout.rows || 24
480
- const getFrame =
481
- context.sprite === 'cat'
482
- ? (index) => getNyanFrame(index, columns)
483
- : (index) => getBananaFrame(index, columns, rows)
484
- const frameMs = context.sprite === 'cat' ? NYAN_FRAME_MS : BANANA_FRAME_MS
543
+ const logRows = Math.min(AGENT_LOG_ROWS, Math.max(3, rows - 10))
544
+ const spriteRows = Math.max(10, rows - logRows)
545
+ const sprite = {
546
+ banana: {
547
+ render: (index) => getBananaFrame(index, columns, spriteRows),
548
+ frameMs: BANANA_FRAME_MS,
549
+ },
550
+ cat: {
551
+ render: (index) => getNyanFrame(index, columns),
552
+ frameMs: NYAN_FRAME_MS,
553
+ },
554
+ pacman: {
555
+ render: (index) => getPacmanFrame(index, columns, spriteRows),
556
+ frameMs: PACMAN_FRAME_MS,
557
+ },
558
+ upyachka: {
559
+ render: (index) => getUpyachkaFrame(index, columns, spriteRows),
560
+ frameMs: UPYACHKA_FRAME_MS,
561
+ },
562
+ }[context.sprite]
485
563
  process.stderr.write('\x1b[?7l')
486
564
 
487
565
  const render = () => {
488
- logUpdateStderr(`${getFrame(frameIndex)}\n${formatText()}`)
566
+ const waiting = phase !== 'receiving'
567
+ const extra = waiting ? `Thinking… ${elapsedSeconds()}s` : ''
568
+ const logBlock = formatLogBlock(logLines, columns, logRows, extra)
569
+ logUpdateStderr(
570
+ `${sprite.render(frameIndex)}\n${formatText()}${logBlock ? `\n${logBlock}` : ''}`,
571
+ )
489
572
  frameIndex++
490
573
  }
491
574
 
492
- const frameTimer = setInterval(render, frameMs)
575
+ const frameTimer = setInterval(render, sprite.frameMs)
493
576
  const stepTimer = setInterval(() => {
494
577
  if (phase === 'early') {
495
578
  if (earlyStepIndex < earlySteps.length - 1) {
@@ -497,14 +580,14 @@ async function withAgentProgress(context, callback) {
497
580
  } else {
498
581
  phase = 'waiting'
499
582
  }
500
- } else if (phase === 'waiting') {
583
+ } else if (phase !== 'receiving') {
501
584
  waitStepIndex++
502
585
  }
503
586
  }, AGENT_STEP_MS)
504
587
 
505
588
  try {
506
589
  render()
507
- return await callback({ onAgentActivity })
590
+ return await callback({ onAgentActivity: reportActivity })
508
591
  } finally {
509
592
  clearInterval(frameTimer)
510
593
  clearInterval(stepTimer)
@@ -513,6 +596,16 @@ async function withAgentProgress(context, callback) {
513
596
  }
514
597
  }
515
598
 
599
+ function formatLogBlock(lines, columns, logRows, extra = '') {
600
+ const all = extra ? [...lines, extra] : lines
601
+ if (!all.length) return ''
602
+ const width = Math.max(20, columns || 80)
603
+ return all
604
+ .slice(-logRows)
605
+ .map((line) => clipLog(line, width))
606
+ .join('\n')
607
+ }
608
+
516
609
  function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
517
610
  const cmd = resolveAgent()
518
611
  const args = [
@@ -520,7 +613,8 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
520
613
  '--mode',
521
614
  'ask',
522
615
  '--output-format',
523
- 'text',
616
+ 'stream-json',
617
+ '--stream-partial-output',
524
618
  '--trust',
525
619
  '--workspace',
526
620
  cwd,
@@ -531,6 +625,9 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
531
625
  args.push(prompt)
532
626
 
533
627
  if (verbose) process.stderr.write(`Using: ${cmd}\n`)
628
+ if (onAgentActivity) {
629
+ onAgentActivity({ log: `Sending prompt (${prompt.length} chars)` })
630
+ }
534
631
 
535
632
  return new Promise((resolve, reject) => {
536
633
  const child = spawn(cmd, args, {
@@ -538,44 +635,240 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
538
635
  stdio: ['ignore', 'pipe', 'pipe'],
539
636
  })
540
637
 
541
- let stdout = ''
638
+ const stream = {
639
+ buffer: '',
640
+ lastAssistant: '',
641
+ raw: '',
642
+ result: '',
643
+ thinking: '',
644
+ }
542
645
  let stderr = ''
646
+ let settled = false
647
+
648
+ const stopChild = (signal = 'SIGTERM') => {
649
+ if (child.exitCode !== null || child.killed) return
650
+ child.kill(signal)
651
+ }
652
+
653
+ const finish = (error, value) => {
654
+ if (settled) return
655
+ settled = true
656
+ process.removeListener('SIGINT', onSignal)
657
+ process.removeListener('SIGTERM', onSignal)
658
+ if (error) reject(error)
659
+ else resolve(value)
660
+ }
661
+
662
+ const onSignal = () => {
663
+ stopChild('SIGTERM')
664
+ finish(new Error('Interrupted'))
665
+ }
666
+ process.once('SIGINT', onSignal)
667
+ process.once('SIGTERM', onSignal)
543
668
 
544
669
  child.stdout.on('data', (chunk) => {
545
- stdout += chunk
546
- if (onAgentActivity) onAgentActivity(stdout.length)
547
- if (verbose) process.stderr.write(chunk)
670
+ const text = String(chunk)
671
+ stream.raw += text
672
+ const activities = consumeAgentStdout(stream, text)
673
+ for (const activity of activities) {
674
+ if (onAgentActivity) onAgentActivity(activity)
675
+ }
548
676
  })
549
677
  child.stderr.on('data', (chunk) => {
550
- stderr += chunk
551
- if (verbose) process.stderr.write(chunk)
678
+ const text = String(chunk)
679
+ stderr += text
680
+ const line = lastNonEmptyLine(text)
681
+ if (line && onAgentActivity) onAgentActivity({ log: line })
682
+ if (verbose) process.stderr.write(text)
552
683
  })
553
684
 
554
- const timeout = setTimeout(() => {
555
- child.kill('SIGTERM')
556
- reject(
557
- new Error(
558
- `Cursor Agent timed out after ${AGENT_TIMEOUT_MS / 1000}s. Check agent login / network.`,
559
- ),
560
- )
561
- }, AGENT_TIMEOUT_MS)
562
-
563
685
  child.on('error', (error) => {
564
- clearTimeout(timeout)
565
- reject(error)
686
+ finish(error)
566
687
  })
567
688
 
568
689
  child.on('close', (code) => {
569
- clearTimeout(timeout)
690
+ consumeAgentStdout(stream, '\n')
691
+ const raw = (
692
+ stream.result ||
693
+ stream.lastAssistant ||
694
+ stream.buffer ||
695
+ stream.raw
696
+ ).trim()
570
697
  if (code) {
571
- reject(new Error(formatAgentError(stderr, stdout)))
698
+ finish(new Error(formatAgentError(stderr, raw)))
699
+ return
700
+ }
701
+ if (!raw) {
702
+ finish(new Error(formatAgentError(stderr, '')))
572
703
  return
573
704
  }
574
- resolve(stdout.trim())
705
+ finish(null, raw)
575
706
  })
576
707
  })
577
708
  }
578
709
 
710
+ function consumeAgentStdout(stream, chunk) {
711
+ stream.buffer += chunk
712
+ const lines = stream.buffer.split('\n')
713
+ stream.buffer = lines.pop() || ''
714
+ const activities = []
715
+ for (const line of lines) {
716
+ const activity = applyAgentLine(stream, line)
717
+ if (activity) activities.push(activity)
718
+ }
719
+ return activities
720
+ }
721
+
722
+ function applyAgentLine(stream, line) {
723
+ const trimmed = line.trim()
724
+ if (!trimmed) return null
725
+
726
+ let event
727
+ try {
728
+ event = JSON.parse(trimmed)
729
+ } catch {
730
+ if (!stream.result) stream.result = trimmed
731
+ return {
732
+ charCount: trimmed.length,
733
+ log: trimmed,
734
+ stream: true,
735
+ }
736
+ }
737
+
738
+ if (!event || typeof event !== 'object') return null
739
+ if (event.type === 'user') return null
740
+
741
+ if (event.type === 'thinking') {
742
+ const text = extractThinkingText(event)
743
+ if (event.subtype === 'completed') {
744
+ return stream.thinking
745
+ ? {
746
+ log: `Thinking: ${stream.thinking}`,
747
+ stream: true,
748
+ streamKey: 'thinking',
749
+ }
750
+ : { log: 'Finished thinking' }
751
+ }
752
+ if (text) {
753
+ stream.thinking += text
754
+ return {
755
+ log: `Thinking: ${stream.thinking}`,
756
+ stream: true,
757
+ streamKey: 'thinking',
758
+ }
759
+ }
760
+ return { log: 'Thinking' }
761
+ }
762
+
763
+ if (event.type === 'result' && typeof event.result === 'string') {
764
+ stream.result = event.result
765
+ return { charCount: stream.result.length, log: 'Agent finished' }
766
+ }
767
+
768
+ if (event.type === 'assistant') {
769
+ if (event.model_call_id != null) return null
770
+ const thinking = extractContentText(event.message?.content, 'thinking')
771
+ if (thinking) {
772
+ stream.thinking += thinking
773
+ return {
774
+ log: `Thinking: ${stream.thinking}`,
775
+ stream: true,
776
+ streamKey: 'thinking',
777
+ }
778
+ }
779
+ const text = extractAssistantText(event)
780
+ if (!text) return null
781
+ if (typeof event.timestamp_ms === 'number') stream.lastAssistant += text
782
+ else stream.lastAssistant = text
783
+ return {
784
+ charCount: stream.lastAssistant.length,
785
+ log: stream.lastAssistant,
786
+ stream: true,
787
+ streamKey: 'assistant',
788
+ }
789
+ }
790
+
791
+ const log = describeAgentEvent(event)
792
+ if (log) return { log }
793
+ if (event.type) {
794
+ return {
795
+ log: event.subtype
796
+ ? `${event.type}: ${event.subtype}`
797
+ : String(event.type),
798
+ }
799
+ }
800
+ return null
801
+ }
802
+
803
+ function extractAssistantText(event) {
804
+ return extractContentText(event.message?.content, 'text')
805
+ }
806
+
807
+ function extractThinkingText(event) {
808
+ if (typeof event.text === 'string' && event.text) return event.text
809
+ if (typeof event.delta === 'string' && event.delta) return event.delta
810
+ return (
811
+ extractContentText(event.message?.content, 'thinking') ||
812
+ extractContentText(event.content, 'thinking') ||
813
+ extractContentText(event.message?.content, 'text')
814
+ )
815
+ }
816
+
817
+ function extractContentText(content, type) {
818
+ if (!Array.isArray(content)) return ''
819
+ return content
820
+ .filter((part) => part?.type === type && typeof part.text === 'string')
821
+ .map((part) => part.text)
822
+ .join('')
823
+ }
824
+
825
+ function describeAgentEvent(event) {
826
+ if (event.type === 'system' && event.subtype === 'init') {
827
+ return event.model ? `Agent started (${event.model})` : 'Agent started'
828
+ }
829
+ if (event.type === 'tool_call') {
830
+ return describeToolCall(event.tool_call, event.subtype === 'completed')
831
+ }
832
+ return ''
833
+ }
834
+
835
+ function describeToolCall(toolCall, done) {
836
+ if (!toolCall || typeof toolCall !== 'object') {
837
+ return done ? 'Tool done' : 'Tool call'
838
+ }
839
+ const [key, payload] = Object.entries(toolCall)[0] || []
840
+ if (key === 'function' && payload?.name) {
841
+ return done ? `Done ${payload.name}` : payload.name
842
+ }
843
+ const args =
844
+ payload?.args && typeof payload.args === 'object' ? payload.args : {}
845
+ const name = (key || 'tool').replace(/ToolCall$/, '')
846
+ const detail =
847
+ args.path ||
848
+ args.file_path ||
849
+ args.command ||
850
+ args.query ||
851
+ args.pattern ||
852
+ args.glob ||
853
+ ''
854
+ const label = detail ? `${name} ${detail}` : name
855
+ return done ? `Done ${label}` : `${label}`
856
+ }
857
+
858
+ function lastNonEmptyLine(text) {
859
+ const lines = String(text)
860
+ .split(/\r?\n/)
861
+ .map((line) => line.trim())
862
+ .filter(Boolean)
863
+ return lines.at(-1) || ''
864
+ }
865
+
866
+ function clipLog(text, max = AGENT_LOG_MAX) {
867
+ const oneLine = String(text).replace(/\s+/g, ' ').trim()
868
+ if (oneLine.length <= max) return oneLine
869
+ return `…${oneLine.slice(-(max - 1))}`
870
+ }
871
+
579
872
  function formatAgentError(stderr, stdout) {
580
873
  const text = (stderr || stdout || AGENT_MISSING).trim()
581
874
  if (/authentication required/i.test(text)) {
@@ -887,6 +1180,11 @@ function sleepSync(ms) {
887
1180
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
888
1181
  }
889
1182
 
1183
+ function formatElapsed(startedAt) {
1184
+ const elapsed = Math.max(0, Math.floor((Date.now() - startedAt) / 1000))
1185
+ return `${elapsed}s`
1186
+ }
1187
+
890
1188
  function indexLockPath(cwd) {
891
1189
  const result = spawnSync('git', ['rev-parse', '--git-path', 'index.lock'], {
892
1190
  cwd: cwd,
@@ -0,0 +1,140 @@
1
+ // High-resolution terminal sprites reconstructed from the supplied reference.
2
+ // Two vertical source pixels share one terminal cell via half-block glyphs.
3
+
4
+ const RESET = '\x1b[0m'
5
+ const FRAME_WIDTH = 80
6
+ const FRAME_HEIGHT = 16
7
+ const FRAME_COUNT = 82
8
+ const SPRITE_WIDTH = 14
9
+ const SPRITE_HEIGHT = 16
10
+ const COLORS = { B: 33, C: 51, O: 208, P: 218, R: 196, W: 231, Y: 226 }
11
+
12
+ const GHOST = [
13
+ '................',
14
+ '......XXXX......',
15
+ '....XXXXXXXX....',
16
+ '...XXXXXXXXXX...',
17
+ '..XXXXXXXXXXXX..',
18
+ '..XWWXXXXWWWXXX.',
19
+ '..WWWWXXWWWWXX..',
20
+ '..BBWWXXBBWWXX..',
21
+ '.XBBWWXXBBWWXXX.',
22
+ '.XXWWXXXXWWXXXX.',
23
+ '.XXXXXXXXXXXXXX.',
24
+ '.XXXXXXXXXXXXXX.',
25
+ '.XXXXXXXXXXXXXX.',
26
+ '.XXXXXXXXXXXXXX.',
27
+ '.XXXXXXXXXXXXXX.',
28
+ '.XX.XXX..XXX.XX.',
29
+ '.X...XX..XX...X.',
30
+ '................',
31
+ ]
32
+
33
+ const PACMAN_CLOSED = [
34
+ '................',
35
+ '.....YYYYYY.....',
36
+ '...YYYYYYYYYY...',
37
+ '..YYYYYYYYYYYY..',
38
+ '.YYYYYYYYYYYYYY.',
39
+ 'YYYYYYYYYYYYYYYY',
40
+ 'YYYYYYYYYYYYYYYY',
41
+ 'YYYYYYYYYYYYYYYY',
42
+ 'YYYYYYYYYYYYYYYY',
43
+ 'YYYYYYYYYYYYYYYY',
44
+ 'YYYYYYYYYYYYYYYY',
45
+ 'YYYYYYYYYYYYYYYY',
46
+ 'YYYYYYYYYYYYYYYY',
47
+ '.YYYYYYYYYYYYYY.',
48
+ '..YYYYYYYYYYYY..',
49
+ '...YYYYYYYYYY...',
50
+ '.....YYYYYY.....',
51
+ '................',
52
+ ]
53
+
54
+ const PACMAN_OPEN = [
55
+ '................',
56
+ '.....YYYYYY.....',
57
+ '...YYYYYYYYYY...',
58
+ '..YYYYYYYYYYYY..',
59
+ '.YYYYYYYYYYYYYY.',
60
+ 'YYYYYYYYYYYYYYYY',
61
+ '..YYYYYYYYYYYYYY',
62
+ '....YYYYYYYYYYYY',
63
+ '......YYYYYYYYYY',
64
+ '........YYYYYYYY',
65
+ '......YYYYYYYYYY',
66
+ '....YYYYYYYYYYYY',
67
+ '..YYYYYYYYYYYYYY',
68
+ '.YYYYYYYYYYYYYY.',
69
+ '..YYYYYYYYYYYY..',
70
+ '...YYYYYYYYYY...',
71
+ '.....YYYYYY.....',
72
+ '................',
73
+ ]
74
+
75
+ function drawSprite(canvas, sprite, left, color) {
76
+ const columns = pickIndexes(sprite[0].length, SPRITE_WIDTH)
77
+ const rows = pickIndexes(sprite.length, SPRITE_HEIGHT)
78
+ for (let y = 0; y < rows.length; y++) {
79
+ for (let x = 0; x < columns.length; x++) {
80
+ const cell = sprite[rows[y]][columns[x]]
81
+ const targetX = left + x
82
+ if (cell === '.' || targetX < 0 || targetX >= FRAME_WIDTH) continue
83
+ canvas[y][targetX] = cell === 'X' ? color : cell
84
+ }
85
+ }
86
+ }
87
+
88
+ function buildFrame(index) {
89
+ const canvas = Array.from({ length: FRAME_HEIGHT }, () =>
90
+ Array(FRAME_WIDTH).fill('.'),
91
+ )
92
+ const pacmanLeft = FRAME_WIDTH - 7 - index * 2
93
+ const pacman = Math.floor(index / 2) % 2 ? PACMAN_CLOSED : PACMAN_OPEN
94
+ drawSprite(canvas, pacman, pacmanLeft, 'Y')
95
+
96
+ for (const [ghostIndex, color] of ['R', 'C', 'P', 'O'].entries()) {
97
+ drawSprite(canvas, GHOST, pacmanLeft + 20 + ghostIndex * 18, color)
98
+ }
99
+ return canvas
100
+ }
101
+
102
+ function pickIndexes(srcSize, maxSize) {
103
+ if (srcSize <= maxSize) {
104
+ return Array.from({ length: srcSize }, (_, index) => index)
105
+ }
106
+ return Array.from({ length: maxSize }, (_, index) =>
107
+ Math.round((index * (srcSize - 1)) / (maxSize - 1)),
108
+ )
109
+ }
110
+
111
+ function paint(top, bottom) {
112
+ const topColor = COLORS[top]
113
+ const bottomColor = COLORS[bottom]
114
+ if (topColor === undefined && bottomColor === undefined) return ' '
115
+ if (topColor === bottomColor) return `\x1b[48;5;${topColor}m \x1b[0m`
116
+ if (topColor === undefined) return `\x1b[38;5;${bottomColor}m▄\x1b[0m`
117
+ if (bottomColor === undefined) return `\x1b[38;5;${topColor}m▀\x1b[0m`
118
+ return `\x1b[38;5;${topColor};48;5;${bottomColor}m▀\x1b[0m`
119
+ }
120
+
121
+ export function getPacmanFrame(index, terminalColumns = 80, terminalRows = 24) {
122
+ const frame = buildFrame(index % FRAME_COUNT)
123
+ const maxColumns = Math.max(16, (terminalColumns || 80) - 1)
124
+ const maxLogicalRows = Math.max(4, ((terminalRows || 24) - 2) * 2)
125
+ const columns = pickIndexes(FRAME_WIDTH, maxColumns)
126
+ const rowCount = Math.min(FRAME_HEIGHT, maxLogicalRows)
127
+ const rows = pickIndexes(FRAME_HEIGHT, rowCount - (rowCount % 2))
128
+ const lines = []
129
+
130
+ for (let index = 0; index < rows.length; index += 2) {
131
+ const top = frame[rows[index]]
132
+ const bottom = frame[rows[index + 1]]
133
+ lines.push(columns.map((x) => paint(top[x], bottom[x])).join('') + RESET)
134
+ }
135
+ return lines.join('\n')
136
+ }
137
+
138
+ export function getPacmanFrameCount() {
139
+ return FRAME_COUNT
140
+ }
@@ -0,0 +1,229 @@
1
+ // Five frames extracted from the classic upyachka GIF.
2
+ // Default-color block pixels stay visible on light and dark terminals.
3
+
4
+ const FRAME_WIDTH = 29
5
+ const FRAME_HEIGHT = 36
6
+
7
+ const FRAMES = [
8
+ [
9
+ '..................#..........',
10
+ '................#####........',
11
+ '...............#######.......',
12
+ '............#..##....##......',
13
+ '............#####....##......',
14
+ '...........######....##......',
15
+ '...........##.####.###.......',
16
+ '...........##...#####........',
17
+ '...........##....###.........',
18
+ '...........##....###.........',
19
+ '...........##...#####........',
20
+ '...........#....##.##........',
21
+ '.............#####..#........',
22
+ '..............########.......',
23
+ '...............##.####.......',
24
+ '..............##.............',
25
+ '..............##.............',
26
+ '.............######..........',
27
+ '.............##########......',
28
+ '.............##....#####.....',
29
+ '.............##.......##.....',
30
+ '.............##.......##.....',
31
+ '.............##......##......',
32
+ '.............##......##......',
33
+ '.............##......##......',
34
+ '.............##......##......',
35
+ '.............##.....##.......',
36
+ '.............##.....##.......',
37
+ '.............##.....##.......',
38
+ '.............##..............',
39
+ '.............##..............',
40
+ '.............##..............',
41
+ '.............##..............',
42
+ '.............##..............',
43
+ '.............##..............',
44
+ '..............#..............',
45
+ ],
46
+ [
47
+ '...............##............',
48
+ '.............######..........',
49
+ '............###..###.........',
50
+ '............##....##.##......',
51
+ '............##....#####......',
52
+ '............##....######.....',
53
+ '.............#######..##.....',
54
+ '..............#####...##.....',
55
+ '...............###....##.....',
56
+ '...............###....##.....',
57
+ '..............##.##...##.....',
58
+ '..............##.##....#.....',
59
+ '.............##.#####........',
60
+ '.............########........',
61
+ '.............####.##.........',
62
+ '...................##........',
63
+ '...................##........',
64
+ '...............#######.......',
65
+ '............##########.......',
66
+ '...........#####....##.......',
67
+ '...........##.......##.......',
68
+ '............##......##.......',
69
+ '............##......##.......',
70
+ '............##......##.......',
71
+ '............##......##.......',
72
+ '............##......##.......',
73
+ '.............##.....##.......',
74
+ '.............##.....##.......',
75
+ '.............##.....##.......',
76
+ '....................##.......',
77
+ '....................##.......',
78
+ '....................##.......',
79
+ '....................##.......',
80
+ '....................##.......',
81
+ '....................##.......',
82
+ '....................#........',
83
+ ],
84
+ [
85
+ '.............................',
86
+ '.............................',
87
+ '.............................',
88
+ '.............................',
89
+ '.............................',
90
+ '.............................',
91
+ '..............#..............',
92
+ '............#####............',
93
+ '...........###.###...........',
94
+ '...........##...##...........',
95
+ '..........##.....#...........',
96
+ '...........##...##...........',
97
+ '.......###############.......',
98
+ '.......###############.......',
99
+ '.......##....###....##.......',
100
+ '........##....#.....##.......',
101
+ '........##....##....#........',
102
+ '.........##...##...##........',
103
+ '.........##...##...##........',
104
+ '..........#...##...##........',
105
+ '..............##.............',
106
+ '..............##.............',
107
+ '..............##.............',
108
+ '..............##.............',
109
+ '..........###########........',
110
+ '......#######...########.....',
111
+ '.....#####..........####.....',
112
+ '.....##...............##.....',
113
+ '......##..............##.....',
114
+ '......##.............##......',
115
+ '......##.............##......',
116
+ '......##.............##......',
117
+ '.......##...........##.......',
118
+ '.......##...........##.......',
119
+ '.......##...........##.......',
120
+ '.......##....................',
121
+ ],
122
+ [
123
+ '.............................',
124
+ '............#####............',
125
+ '...........#######...........',
126
+ '###.......###...##...........',
127
+ '.####.....##....##.......####',
128
+ '...####...##....##....######.',
129
+ '.....########..##########....',
130
+ '.......###############.......',
131
+ '............#####............',
132
+ '.............##..............',
133
+ '.............##..............',
134
+ '..............#..............',
135
+ '..............#..............',
136
+ '..............#..............',
137
+ '..............#..............',
138
+ '..............##.............',
139
+ '..............##.............',
140
+ '..............##.............',
141
+ '..............##.............',
142
+ '.............###.............',
143
+ '............#####............',
144
+ '............##.###...........',
145
+ '...........##...##...........',
146
+ '...........##....##..........',
147
+ '..........##.....##..........',
148
+ '.........##.......##.........',
149
+ '.........##.......##.........',
150
+ '.........##........##........',
151
+ '........###........##........',
152
+ '........##.........##........',
153
+ '........##.........##........',
154
+ '........##.........##........',
155
+ '.......##...........##.......',
156
+ '.......##...........##.......',
157
+ '.......##...........##.......',
158
+ '.............................',
159
+ ],
160
+ [
161
+ '.............................',
162
+ '.............................',
163
+ '.............................',
164
+ '.............................',
165
+ '.............................',
166
+ '.............................',
167
+ '.............................',
168
+ '.............................',
169
+ '.............................',
170
+ '.............................',
171
+ '.............................',
172
+ '............###..............',
173
+ '...........######............',
174
+ '..........###..###...........',
175
+ '..........##....##...........',
176
+ '..........##....##...........',
177
+ '..........##...###...........',
178
+ '...........######............',
179
+ '............####.............',
180
+ '...........#####.............',
181
+ '..........#######............',
182
+ '.........#########...........',
183
+ '........##########...........',
184
+ '........###..######..........',
185
+ '.............##..##..........',
186
+ '.....###.....##......###.....',
187
+ '.....######..##...#####......',
188
+ '......#################......',
189
+ '......##...#######...##......',
190
+ '......##.....##.....##.......',
191
+ '.......##...........##.......',
192
+ '.......##..........##........',
193
+ '.......###.........##........',
194
+ '........##.........##........',
195
+ '........##.........#.........',
196
+ '.............................',
197
+ ],
198
+ ]
199
+
200
+ function pickIndexes(srcSize, maxSize) {
201
+ if (srcSize <= maxSize) {
202
+ return Array.from({ length: srcSize }, (_, index) => index)
203
+ }
204
+ return Array.from({ length: maxSize }, (_, index) =>
205
+ Math.round((index * (srcSize - 1)) / (maxSize - 1)),
206
+ )
207
+ }
208
+
209
+ export function getUpyachkaFrame(
210
+ index,
211
+ terminalColumns = 80,
212
+ terminalRows = 24,
213
+ ) {
214
+ const maxColumns = Math.max(8, Math.floor((terminalColumns - 1) / 2))
215
+ const maxRows = Math.max(8, (terminalRows || 24) - 2)
216
+ const columns = pickIndexes(FRAME_WIDTH, maxColumns)
217
+ const rows = pickIndexes(FRAME_HEIGHT, maxRows)
218
+ const frame = FRAMES[index % FRAMES.length]
219
+
220
+ return rows
221
+ .map((y) =>
222
+ columns.map((x) => (frame[y][x] === '#' ? '██' : ' ')).join(''),
223
+ )
224
+ .join('\n')
225
+ }
226
+
227
+ export function getUpyachkaFrameCount() {
228
+ return FRAMES.length
229
+ }