@itcase/config 1.6.80 → 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.80",
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
 
@@ -213,6 +220,8 @@ function parseArgs(argv) {
213
220
  else if (arg === '--staged') option.staged = true
214
221
  else if (arg === '--cat' || arg === '--nyan') option.sprite = 'cat'
215
222
  else if (arg === '--banana') option.sprite = 'banana'
223
+ else if (arg === '--upyachka') option.sprite = 'upyachka'
224
+ else if (arg === '--pacman') option.sprite = 'pacman'
216
225
  else if (arg === '--type') option.type = argv[++i] || ''
217
226
  else if (arg === '--model') option.model = argv[++i] || ''
218
227
  else if (arg.startsWith('--type=')) option.type = arg.slice(7)
@@ -223,8 +232,9 @@ function parseArgs(argv) {
223
232
  }
224
233
 
225
234
  function pickSprite(forced) {
226
- if (forced === 'cat' || forced === 'banana') return forced
227
- 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)]
228
238
  }
229
239
 
230
240
  function assertItcaseConfigPackage(cwd) {
@@ -460,49 +470,109 @@ async function withAgentProgress(context, callback) {
460
470
  let phase = 'early'
461
471
  let receivedChars = 0
462
472
  let frameIndex = 0
473
+ const logLines = []
474
+ const streamByKey = { assistant: -1, thinking: -1 }
463
475
 
464
- const onAgentActivity = (charCount) => {
465
- if (charCount > 0) {
466
- receivedChars = charCount
476
+ const elapsedSeconds = () => Math.floor((Date.now() - startedAt) / 1000)
477
+
478
+ const onAgentActivity = (activity = {}) => {
479
+ if (activity.charCount > 0) {
480
+ receivedChars = activity.charCount
467
481
  phase = 'receiving'
468
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'
469
497
  }
470
498
 
471
499
  const currentLabel = () => {
500
+ const elapsed = elapsedSeconds()
472
501
  if (phase === 'receiving') {
473
502
  return `Receiving response (${receivedChars} chars)`
474
503
  }
475
- if (phase === 'waiting') {
504
+ if (elapsed >= AGENT_STILL_WORKING_S) return 'Agent still working'
505
+ if (phase === 'waiting' || phase === 'working') {
476
506
  return AGENT_WAIT_STEPS[waitStepIndex % AGENT_WAIT_STEPS.length]
477
507
  }
478
508
  return earlySteps[Math.min(earlyStepIndex, earlySteps.length - 1)]
479
509
  }
480
510
 
481
511
  const formatText = () => {
482
- const elapsed = Math.floor((Date.now() - startedAt) / 1000)
483
- 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
+ }
484
526
  }
485
527
 
486
528
  if (!process.stderr.isTTY || context.verbose) {
487
529
  process.stderr.write(`${formatText()}\n`)
488
- 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
+ }
489
539
  }
490
540
 
491
541
  const columns = process.stderr.columns || process.stdout.columns || 80
492
542
  const rows = process.stderr.rows || process.stdout.rows || 24
493
- const getFrame =
494
- context.sprite === 'cat'
495
- ? (index) => getNyanFrame(index, columns)
496
- : (index) => getBananaFrame(index, columns, rows)
497
- 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]
498
563
  process.stderr.write('\x1b[?7l')
499
564
 
500
565
  const render = () => {
501
- 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
+ )
502
572
  frameIndex++
503
573
  }
504
574
 
505
- const frameTimer = setInterval(render, frameMs)
575
+ const frameTimer = setInterval(render, sprite.frameMs)
506
576
  const stepTimer = setInterval(() => {
507
577
  if (phase === 'early') {
508
578
  if (earlyStepIndex < earlySteps.length - 1) {
@@ -510,14 +580,14 @@ async function withAgentProgress(context, callback) {
510
580
  } else {
511
581
  phase = 'waiting'
512
582
  }
513
- } else if (phase === 'waiting') {
583
+ } else if (phase !== 'receiving') {
514
584
  waitStepIndex++
515
585
  }
516
586
  }, AGENT_STEP_MS)
517
587
 
518
588
  try {
519
589
  render()
520
- return await callback({ onAgentActivity })
590
+ return await callback({ onAgentActivity: reportActivity })
521
591
  } finally {
522
592
  clearInterval(frameTimer)
523
593
  clearInterval(stepTimer)
@@ -526,6 +596,16 @@ async function withAgentProgress(context, callback) {
526
596
  }
527
597
  }
528
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
+
529
609
  function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
530
610
  const cmd = resolveAgent()
531
611
  const args = [
@@ -533,7 +613,8 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
533
613
  '--mode',
534
614
  'ask',
535
615
  '--output-format',
536
- 'text',
616
+ 'stream-json',
617
+ '--stream-partial-output',
537
618
  '--trust',
538
619
  '--workspace',
539
620
  cwd,
@@ -544,6 +625,9 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
544
625
  args.push(prompt)
545
626
 
546
627
  if (verbose) process.stderr.write(`Using: ${cmd}\n`)
628
+ if (onAgentActivity) {
629
+ onAgentActivity({ log: `Sending prompt (${prompt.length} chars)` })
630
+ }
547
631
 
548
632
  return new Promise((resolve, reject) => {
549
633
  const child = spawn(cmd, args, {
@@ -551,44 +635,240 @@ function askAgent(cwd, model, prompt, verbose, onAgentActivity) {
551
635
  stdio: ['ignore', 'pipe', 'pipe'],
552
636
  })
553
637
 
554
- let stdout = ''
638
+ const stream = {
639
+ buffer: '',
640
+ lastAssistant: '',
641
+ raw: '',
642
+ result: '',
643
+ thinking: '',
644
+ }
555
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)
556
668
 
557
669
  child.stdout.on('data', (chunk) => {
558
- stdout += chunk
559
- if (onAgentActivity) onAgentActivity(stdout.length)
560
- 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
+ }
561
676
  })
562
677
  child.stderr.on('data', (chunk) => {
563
- stderr += chunk
564
- 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)
565
683
  })
566
684
 
567
- const timeout = setTimeout(() => {
568
- child.kill('SIGTERM')
569
- reject(
570
- new Error(
571
- `Cursor Agent timed out after ${AGENT_TIMEOUT_MS / 1000}s. Check agent login / network.`,
572
- ),
573
- )
574
- }, AGENT_TIMEOUT_MS)
575
-
576
685
  child.on('error', (error) => {
577
- clearTimeout(timeout)
578
- reject(error)
686
+ finish(error)
579
687
  })
580
688
 
581
689
  child.on('close', (code) => {
582
- clearTimeout(timeout)
690
+ consumeAgentStdout(stream, '\n')
691
+ const raw = (
692
+ stream.result ||
693
+ stream.lastAssistant ||
694
+ stream.buffer ||
695
+ stream.raw
696
+ ).trim()
583
697
  if (code) {
584
- 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, '')))
585
703
  return
586
704
  }
587
- resolve(stdout.trim())
705
+ finish(null, raw)
588
706
  })
589
707
  })
590
708
  }
591
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
+
592
872
  function formatAgentError(stderr, stdout) {
593
873
  const text = (stderr || stdout || AGENT_MISSING).trim()
594
874
  if (/authentication required/i.test(text)) {
@@ -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
+ }