clarity-ai 6.5.6 → 6.7.0

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,31 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## 6.7.0 (2026-06-06)
6
+
7
+ ### Premium Ink+React TUI Engine — zero-bleed grid
8
+ - **Hard-locked 3-tier grid**: Header(1) + Viewport(fill) + Dock(4) — absolute layout isolation
9
+ - **Floating picker overlays** via `position: absolute` — CommandPicker/ModelPicker never shift the message stream
10
+ - **Box-drawing borders** (`┌─┐│└─┘`) on all overlay panels
11
+ - **Orange `#FF6B35` selection bars** — full-width active item highlight
12
+ - **Gradient CLARITY header** via gradient-string (orange→blue)
13
+ - **Alternate screen buffer** (`fullscreen: true`) + SIGINT/SIGTERM cleanup
14
+ - **`string-width` render protection** — correct visual width for Unicode/emoji
15
+ - **Permanent ASCII logo** centered in empty viewport
16
+ - **ansi-escapes** for cursor hide/show, screen clear on boot
17
+
18
+ ## 6.6.0 (2026-06-06)
19
+
20
+ ### Premium UI rebuild + DeepSeek R1 reasoning models
21
+ - **Models**: Flash → DeepSeek-R1-Distill-Qwen-1.5B (reasoning), Heavy → DeepSeek-R1-Distill-Qwen-7B (reasoning)
22
+ - **Permanent center CLARITY ASCII logo** — stays as background until first message
23
+ - **Box-drawing overlays** (`┌─┐│└─┘`) on CommandPicker and ModelPicker
24
+ - **Non-bouncing 3-tier viewport** — topBar(1) + viewport(fill) + dock(4)
25
+ - **Cleaner message rendering** — `│` prefix for assistant text, no wasted rows
26
+ - **Tool log compression** — completed tools collapse to single-line chips
27
+ - **Orange selection bar** (`#FF6B35`) on all pickers
28
+ - **DeepSeek R1 reasoning** — model outputs `<think>` blocks naturally
29
+
5
30
  ## 6.5.6 (2026-06-06)
6
31
 
7
32
  ### Fix first-request timeout — model download needs >8s
package/bin/clarity.js CHANGED
@@ -4,6 +4,7 @@ import { render } from 'ink';
4
4
  import { App } from '../src/components/AppRoot.js';
5
5
  import { hasKey } from '../src/config/keys.js';
6
6
  import { createInterface } from 'readline';
7
+ import ansiEscapes from 'ansi-escapes';
7
8
 
8
9
  process.stdin.resume();
9
10
  process.stdin.setEncoding('utf8');
@@ -24,29 +25,39 @@ async function main() {
24
25
  process.stdin.resume();
25
26
  }
26
27
 
28
+ process.stdout.write(ansiEscapes.clearScreen);
29
+ process.stdout.write(ansiEscapes.cursorHide);
30
+
27
31
  const config = { provider, model: process.env.CLARITY_MODEL || 'groq/llama-3.3-70b-versatile' };
28
32
 
29
- const { clear } = render(React.createElement(App, { config }), {
33
+ const { clear, waitUntilExit } = render(React.createElement(App, { config }), {
30
34
  fullscreen: true,
31
35
  patchConsole: false,
36
+ exitOnCtrlC: false,
32
37
  });
33
38
 
34
- setInterval(() => {}, 2 ** 31 - 1);
39
+ const keepAlive = setInterval(() => {}, 2 ** 31 - 1);
35
40
 
36
41
  function cleanup() {
42
+ clearInterval(keepAlive);
43
+ process.stdout.write(ansiEscapes.cursorShow);
44
+ process.stdout.write('\x1b[0m');
37
45
  try { clear(); } catch {}
38
- process.stdout.write('\x1b[?25h\x1b[0m');
39
46
  process.exit(0);
40
47
  }
41
48
 
42
49
  process.on('SIGINT', () => cleanup());
43
50
  process.on('SIGTERM', () => cleanup());
51
+ process.on('exit', () => {
52
+ process.stdout.write(ansiEscapes.cursorShow);
53
+ });
44
54
 
45
55
  await new Promise(() => {});
46
56
  }
47
57
 
48
58
  main().catch(err => {
49
- process.stdout.write('\x1b[?25h\x1b[0m');
59
+ process.stdout.write(ansiEscapes.cursorShow);
60
+ process.stdout.write('\x1b[0m');
50
61
  console.error('\n\x1b[31mFatal error:\x1b[0m', err.message);
51
62
  process.exit(1);
52
63
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "6.5.6",
3
+ "version": "6.7.0",
4
4
  "description": "CLARITY — terminal AI agent with local GGUF inference on HF Spaces",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,6 +23,10 @@
23
23
  "react": "^18",
24
24
  "ink-spinner": "^5",
25
25
  "chalk": "^5",
26
- "wrap-ansi": "^9"
26
+ "wrap-ansi": "^9",
27
+ "gradient-string": "^3",
28
+ "figures": "^6",
29
+ "ansi-escapes": "^7",
30
+ "string-width": "^7"
27
31
  }
28
32
  }
@@ -34,11 +34,9 @@ export function App({ config }) {
34
34
  const stateRef = useRef(state);
35
35
  const modelRef = useRef(model);
36
36
  const providerRef = useRef(provider);
37
- const streamRef = useRef(streamContent);
38
37
  stateRef.current = state;
39
38
  modelRef.current = model;
40
39
  providerRef.current = provider;
41
- streamRef.current = streamContent;
42
40
 
43
41
  const onSubmit = useCallback(async (input) => {
44
42
  if (input === '/exit') { process.exit(0); return; }
@@ -74,14 +72,10 @@ export function App({ config }) {
74
72
  }));
75
73
  }
76
74
 
77
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg },
75
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, height: '100%' },
78
76
  h(Layout, {
79
- state,
80
- streamContent,
81
- model,
82
- provider,
83
- showCommands,
84
- showModels,
77
+ state, streamContent, model, provider,
78
+ showCommands, showModels,
85
79
  onCommandSelect: handleCommandSelect,
86
80
  onModelSelect: handleModelSelect,
87
81
  onCloseCommands: () => setShowCommands(false),
@@ -21,26 +21,28 @@ export function CodeBlock({ code, language }) {
21
21
  const { cols } = getLayout();
22
22
  const maxLines = 20;
23
23
  const visible = lines.slice(0, maxLines);
24
+ const codeWidth = cols - 10;
24
25
 
25
26
  return h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
26
- h(Box, { flexDirection: 'row', backgroundColor: hex.codeBg },
27
- h(Text, { color: langColor, bold: true, backgroundColor: hex.codeBg }, ' ' + lang + ' '),
28
- h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, String(lines.length) + ' lines '),
27
+ h(Box, { height: 1, backgroundColor: hex.codeBg },
28
+ h(Text, { color: langColor, bold: true, backgroundColor: hex.codeBg }, ' ' + lang),
29
+ h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + String(lines.length) + ' lines '),
29
30
  ),
30
31
  h(Box, { flexDirection: 'column', backgroundColor: hex.codeBg },
31
32
  visible.map((line, i) =>
32
- h(Box, { key: i, flexDirection: 'row', backgroundColor: hex.codeBg },
33
+ h(Box, { key: i, height: 1, backgroundColor: hex.codeBg },
33
34
  h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg },
34
- ' ' + String(i + 1).padStart(lnW) + ' '
35
+ ' ' + String(i + 1).padStart(lnW) + ' '
35
36
  ),
36
37
  h(Text, { color: '#C9D1D9', backgroundColor: hex.codeBg, wrap: 'truncate-end' },
37
- (line || ' ').slice(0, cols - 8)
38
+ (line || ' ').slice(0, codeWidth)
38
39
  )
39
40
  )
40
41
  ),
41
42
  lines.length > maxLines
42
- ? h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg },
43
- ' ' + sym.ellipsis + ' ' + (lines.length - maxLines) + ' more lines')
43
+ ? h(Box, { height: 1, backgroundColor: hex.codeBg },
44
+ h(Text, { color: hex.textMuted, backgroundColor: hex.codeBg }, ' ' + sym.ellipsis + ' ' + (lines.length - maxLines) + ' more lines')
45
+ )
44
46
  : null
45
47
  )
46
48
  );
@@ -5,7 +5,7 @@ import { getLayout } from '../config/layout.js';
5
5
  const { createElement: h } = React;
6
6
 
7
7
  const COMMANDS = [
8
- { name: '/keys', desc: 'Set API key for a provider' },
8
+ { name: '/keys', desc: 'Set API key' },
9
9
  { name: '/model', desc: 'Switch model' },
10
10
  { name: '/provider', desc: 'Switch provider' },
11
11
  { name: '/agent', desc: 'Toggle agent mode' },
@@ -29,35 +29,47 @@ export function CommandPicker({ query, onSelect, onClose }) {
29
29
  if (key.upArrow) setIdx(i => Math.max(0, i - 1));
30
30
  if (key.downArrow) setIdx(i => Math.min(filtered.length - 1, i + 1));
31
31
  if (key.return && filtered[idx]) onSelect(filtered[idx].name);
32
- if (key.escape) onClose();
32
+ if (key.escape || key.ctrl && key.c) onClose();
33
33
  if (key.backspace) setSearch(s => s.slice(0, -1));
34
34
  else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
35
35
  });
36
36
 
37
37
  const w = Math.min(cols - 4, 48);
38
38
 
39
- return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt, width: w },
39
+ const items = filtered.map((cmd, i) =>
40
+ h(Box, {
41
+ key: cmd.name, height: 1,
42
+ backgroundColor: i === idx ? hex.selectionBg : hex.bg,
43
+ },
44
+ h(Text, {
45
+ color: i === idx ? hex.selectionText : hex.text,
46
+ bold: i === idx,
47
+ backgroundColor: i === idx ? hex.selectionBg : hex.bg,
48
+ }, ' ' + (i === idx ? '\u276F ' : ' ') + cmd.name + ' ' + cmd.desc)
49
+ )
50
+ );
51
+
52
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
40
53
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
41
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + sym.star + ' ' + (search || 'filter commands...'))
54
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
55
+ sym.box.tl + sym.box.h.repeat(w - 2) + sym.box.tr)
42
56
  ),
43
- filtered.map((cmd, i) =>
44
- h(Box, {
45
- key: cmd.name, height: 1,
46
- backgroundColor: i === idx ? hex.selectionBg : 'transparent',
47
- },
48
- h(Text, {
49
- color: i === idx ? hex.selectionText : hex.text,
50
- bold: i === idx,
51
- backgroundColor: i === idx ? hex.selectionBg : 'transparent',
52
- }, ' ' + cmd.name + ' '),
53
- h(Text, {
54
- color: i === idx ? hex.selectionText : hex.textDim,
55
- backgroundColor: i === idx ? hex.selectionBg : 'transparent',
56
- }, cmd.desc)
57
- )
57
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
58
+ h(Text, { color: hex.gold, backgroundColor: hex.surfaceAlt }, sym.box.v + ' Commands'),
59
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' '.repeat(Math.max(0, w - 14)) + sym.box.v)
58
60
  ),
59
61
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
60
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + sym.arrowU + sym.arrowD + ' nav ' + sym.arrowR + ' select Esc close')
61
- )
62
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
63
+ sym.box.v + ' ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + sym.box.v)
64
+ ),
65
+ h(Box, { flexDirection: 'column', backgroundColor: hex.bg }, ...items),
66
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
67
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
68
+ sym.box.bl + sym.box.h.repeat(w - 2) + sym.box.br)
69
+ ),
70
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
71
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
72
+ ' \u2191\u2193 nav \u2192 select Esc close')
73
+ ),
62
74
  );
63
75
  }
@@ -45,11 +45,11 @@ export function Composer({ provider, model, agentMode, thinking, onSlash, onSubm
45
45
  }
46
46
  });
47
47
 
48
- const rows = [
49
- h(Box, { key: 'sep', height: 1, backgroundColor: hex.surfaceAlt },
50
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + sym.lightH.repeat(Math.max(0, cols - 4)))
51
- ),
52
- ];
48
+ const rows = [];
49
+ rows.push(h(Box, { key: 'sep', height: 1, backgroundColor: hex.surfaceAlt },
50
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
51
+ ' \u250C' + sym.box.h.repeat(Math.max(0, cols - 6)) + '\u2510')
52
+ ));
53
53
 
54
54
  for (let i = 0; i < MAX_ROWS; i++) {
55
55
  const start = i * w;
@@ -60,17 +60,15 @@ export function Composer({ provider, model, agentMode, thinking, onSlash, onSubm
60
60
  color: isPlaceholder ? hex.textMuted : hex.text,
61
61
  backgroundColor: hex.bg,
62
62
  wrap: 'truncate-end',
63
- }, ' ' + sym.triR + ' ' + (seg || (i === 0 && isPlaceholder ? 'type a message...' : ' ')))
63
+ }, ' \u2502 ' + (seg || (i === 0 && isPlaceholder ? 'type a message...' : ' ')))
64
64
  )
65
65
  );
66
66
  }
67
67
 
68
- rows.push(
69
- h(Box, { key: 'st', height: 1, backgroundColor: hex.surfaceAlt },
70
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
71
- ' ' + provider + ' ' + sym.midDot + ' ' + mShort + (agentMode ? ' ' + sym.midDot + ' AGENT' : '') + ' ' + sym.midDot + ' Ctrl+P')
72
- )
73
- );
68
+ rows.push(h(Box, { key: 'st', height: 1, backgroundColor: hex.surfaceAlt },
69
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
70
+ ' \u2514' + sym.box.h + ' ' + provider + ' \u00B7 ' + mShort + (agentMode ? ' \u00B7 AGENT' : '') + ' \u00B7 Ctrl+P')
71
+ ));
74
72
 
75
73
  return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt }, ...rows);
76
74
  }
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { Box } from 'ink';
3
3
  import { hex } from '../config/theme.js';
4
+ import { getLayout } from '../config/layout.js';
4
5
  import { StatusBar } from './StatusBar.js';
5
6
  import { MessageList } from './MessageList.js';
6
7
  import { Composer } from './Composer.js';
@@ -9,29 +10,25 @@ import { ModelPicker } from './ModelPicker.js';
9
10
  const { createElement: h } = React;
10
11
 
11
12
  export function Layout({ state, streamContent, model, provider, showCommands, showModels, onCommandSelect, onModelSelect, onCloseCommands, onCloseModels, onSlash, onSubmit }) {
12
- return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, minHeight: '100%' },
13
+ const { headerHeight, dockHeight, cols } = getLayout();
14
+
15
+ const picker = showCommands || showModels
16
+ ? h(Box, { position: 'absolute', top: headerHeight + 1, left: 2, width: Math.min(cols - 4, 52), backgroundColor: hex.bg, flexDirection: 'column' },
17
+ showCommands ? h(CommandPicker, { query: '', onSelect: onCommandSelect, onClose: onCloseCommands }) : null,
18
+ showModels ? h(ModelPicker, { onSelect: onModelSelect, onClose: onCloseModels }) : null
19
+ )
20
+ : null;
21
+
22
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, height: '100%' },
13
23
  h(StatusBar, { model, provider, agentMode: state.agentMode, thinking: state.thinking }),
14
- h(Box, { flexGrow: 1, flexDirection: 'column' },
24
+ h(Box, { flexGrow: 1, flexDirection: 'column', position: 'relative' },
15
25
  h(MessageList, {
16
- messages: state.messages,
17
- thinking: state.thinking,
18
- streamContent,
19
- agentStatus: state.agentStatus,
26
+ messages: state.messages, thinking: state.thinking,
27
+ streamContent, agentStatus: state.agentStatus,
20
28
  toolExecutions: state.toolExecutions,
21
- })
29
+ }),
30
+ picker
22
31
  ),
23
- showCommands || showModels
24
- ? h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
25
- showCommands ? h(CommandPicker, { query: '', onSelect: onCommandSelect, onClose: onCloseCommands }) : null,
26
- showModels ? h(ModelPicker, { onSelect: onModelSelect, onClose: onCloseModels }) : null
27
- )
28
- : null,
29
- h(Composer, {
30
- provider, model,
31
- agentMode: state.agentMode,
32
- thinking: state.thinking,
33
- onSlash,
34
- onSubmit,
35
- })
32
+ h(Composer, { provider, model, agentMode: state.agentMode, thinking: state.thinking, onSlash, onSubmit })
36
33
  );
37
34
  }
@@ -4,8 +4,8 @@ import { hex, sym } from '../config/theme.js';
4
4
  const { createElement: h } = React;
5
5
 
6
6
  export function LoadingIndicator({ label }) {
7
- return h(Box, { flexDirection: 'row', backgroundColor: hex.surface },
8
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' ' + sym.dot),
7
+ return h(Box, { height: 1, backgroundColor: hex.surface },
8
+ h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' ' + sym.dot),
9
9
  h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' ' + (label || 'processing'))
10
10
  );
11
11
  }
@@ -1,6 +1,6 @@
1
1
  import React, { useMemo } from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
3
+ import { hex, sym, LOGO } from '../config/theme.js';
4
4
  import { getLayout, sliceToViewport, buildLineArray } from '../config/layout.js';
5
5
  const { createElement: h } = React;
6
6
 
@@ -8,47 +8,47 @@ function Line({ type, text, data }) {
8
8
  switch (type) {
9
9
  case 'user_head':
10
10
  return h(Box, { height: 1, backgroundColor: hex.userBg },
11
- h(Text, { color: hex.accent, bold: true, backgroundColor: hex.userBg }, ' ' + sym.bullet + ' YOU')
11
+ h(Text, { color: hex.accent, bold: true, backgroundColor: hex.userBg }, ' \u276F \u25C9 YOU')
12
12
  );
13
13
  case 'user_line':
14
14
  return h(Box, { height: 1, backgroundColor: hex.userBg },
15
- h(Text, { color: hex.text, backgroundColor: hex.userBg, wrap: 'wrap' }, ' ' + (text || ' '))
15
+ h(Text, { color: hex.text, backgroundColor: hex.userBg, wrap: 'wrap' }, ' ' + (text || ' '))
16
16
  );
17
17
  case 'asst_head':
18
18
  return h(Box, { height: 1, backgroundColor: hex.surface },
19
- h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' ' + sym.diamond + ' CLARITY')
19
+ h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' \u25C6 CLARITY')
20
20
  );
21
21
  case 'asst_line':
22
22
  return h(Box, { height: 1, backgroundColor: hex.surface },
23
- h(Text, { color: hex.text, backgroundColor: hex.surface, wrap: 'wrap' }, ' ' + (text || ' '))
23
+ h(Text, { color: hex.text, backgroundColor: hex.surface, wrap: 'wrap' }, ' \u2502 ' + (text || ' '))
24
24
  );
25
25
  case 'asst_foot':
26
26
  return h(Box, { height: 1, backgroundColor: hex.surface },
27
- h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' ' + sym.triR + ' ' + (parseInt(text) < 1000 ? text + 'ms' : (parseInt(text) / 1000).toFixed(1) + 's'))
27
+ h(Text, { color: hex.textDim, backgroundColor: hex.surface }, ' \u25B8 ' + (parseInt(text) < 1000 ? text + 'ms' : (parseInt(text) / 1000).toFixed(1) + 's'))
28
28
  );
29
29
  case 'tool_line':
30
30
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
31
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + (text || ''))
31
+ h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + (text || ''))
32
32
  );
33
33
  case 'sys_line':
34
34
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
35
- h(Text, { color: hex.green, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + (text || ''))
35
+ h(Text, { color: hex.green, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + (text || ''))
36
36
  );
37
37
  case 'err_line':
38
38
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
39
- h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, ' ' + sym.cross + ' ' + (text || ''))
39
+ h(Text, { color: hex.red, backgroundColor: hex.surfaceAlt }, ' ' + sym.cross + ' ' + (text || ''))
40
40
  );
41
41
  case 'stream_head':
42
42
  return h(Box, { height: 1, backgroundColor: hex.surface },
43
- h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' ' + sym.diamond + ' CLARITY')
43
+ h(Text, { color: hex.purple, bold: true, backgroundColor: hex.surface }, ' \u25C6 CLARITY')
44
44
  );
45
45
  case 'stream_status':
46
46
  return h(Box, { height: 1, backgroundColor: hex.surface },
47
- h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' ' + sym.dot + ' ' + (text || ''))
47
+ h(Text, { color: hex.blue, backgroundColor: hex.surface }, ' \u25CF ' + (text || ''))
48
48
  );
49
49
  case 'stream_line':
50
50
  return h(Box, { height: 1, backgroundColor: hex.surface },
51
- h(Text, { color: hex.text, backgroundColor: hex.surface, wrap: 'wrap' }, ' ' + (text || ' '))
51
+ h(Text, { color: hex.text, backgroundColor: hex.surface, wrap: 'wrap' }, ' \u2502 ' + (text || ' '))
52
52
  );
53
53
  default:
54
54
  return null;
@@ -59,43 +59,49 @@ export function MessageList({ messages, thinking, streamContent, agentStatus, to
59
59
  const { viewport, contentWidth } = getLayout();
60
60
 
61
61
  const entries = useMemo(() => {
62
- const e = messages.map(m => ({
62
+ return messages.map(m => ({
63
63
  id: m.id, role: m.role, content: m.content,
64
64
  duration: m.duration, toolName: m.toolName, error: m.error, completed: true,
65
65
  }));
66
- if (thinking || streamContent) {
67
- e.push({
68
- id: 'stream', role: 'streaming',
69
- content: streamContent || '',
70
- status: agentStatus || (thinking ? 'processing...' : ''),
71
- completed: false,
72
- });
73
- }
74
- return e;
75
- }, [messages, thinking, streamContent, agentStatus]);
66
+ }, [messages]);
67
+
68
+ const showLogo = entries.length <= 1 && !thinking && !streamContent;
69
+ const effectiveViewport = showLogo ? Math.max(viewport - 14, 4) : viewport;
76
70
 
77
71
  const { slice, clipIndex, clipLines } = useMemo(
78
- () => sliceToViewport(entries, viewport, contentWidth),
79
- [entries, viewport, contentWidth]
72
+ () => sliceToViewport(entries, effectiveViewport, contentWidth),
73
+ [entries, effectiveViewport, contentWidth]
80
74
  );
81
75
 
82
- const lines = useMemo(
76
+ const rawLines = useMemo(
83
77
  () => buildLineArray(slice, clipIndex, clipLines, contentWidth),
84
78
  [slice, clipIndex, clipLines, contentWidth]
85
79
  );
86
80
 
87
- const padded = [...lines];
88
- for (let i = padded.length; i < viewport; i++) {
89
- padded.unshift({ type: 'empty' });
81
+ const padded = [];
82
+ const logoRows = showLogo ? 14 : 0;
83
+ const fillRows = Math.max(0, effectiveViewport - rawLines.length - logoRows);
84
+ for (let i = 0; i < fillRows; i++) padded.push({ type: 'empty' });
85
+ if (showLogo) {
86
+ for (let i = 0; i < 14; i++) padded.push({ type: 'logo', line: i });
90
87
  }
88
+ for (const ln of rawLines) padded.push(ln);
91
89
 
92
90
  return h(Box, { height: viewport, flexDirection: 'column', overflow: 'hidden' },
93
- padded.map((ln, i) =>
94
- ln.type === 'empty'
95
- ? h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg })
96
- : h(Box, { key: (ln.data?.id || 'l') + '-' + i, height: 1 },
97
- h(Line, { type: ln.type, text: ln.text, data: ln.data })
98
- )
99
- )
91
+ padded.map((ln, i) => {
92
+ if (ln.type === 'empty') return h(Box, { key: 'e' + i, height: 1, backgroundColor: hex.bg });
93
+ if (ln.type === 'logo') {
94
+ const logoLine = LOGO[ln.line] || '';
95
+ return h(Box, { key: 'l' + i, height: 1, backgroundColor: hex.bg },
96
+ h(Text, {
97
+ color: (ln.line >= 1 && ln.line <= 5) || ln.line === 9 ? hex.accent : hex.textMuted,
98
+ backgroundColor: hex.bg,
99
+ }, ' ' + logoLine)
100
+ );
101
+ }
102
+ return h(Box, { key: (ln.data?.id || 'r') + '-' + i, height: 1 },
103
+ h(Line, { type: ln.type, text: ln.text, data: ln.data })
104
+ );
105
+ })
100
106
  );
101
107
  }
@@ -32,37 +32,56 @@ export function ModelPicker({ onSelect, onClose }) {
32
32
  if (key.upArrow) setIdx(i => Math.max(0, i - 1));
33
33
  if (key.downArrow) setIdx(i => Math.min(flat.length - 1, i + 1));
34
34
  if (key.return) { const m = flat[idx]; if (m && !m._header) onSelect(m.id); return; }
35
- if (key.escape) onClose();
35
+ if (key.escape || key.ctrl && key.c) onClose();
36
36
  if (key.backspace) setSearch(s => s.slice(0, -1));
37
37
  else if (input && !key.ctrl && !key.meta) setSearch(s => s + input);
38
38
  });
39
39
 
40
40
  const w = Math.min(cols - 4, 52);
41
41
 
42
- return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt, width: w },
42
+ const items = flat.map((m, i) => {
43
+ if (m._header) {
44
+ return h(Box, { key: 'h-' + m._provider, height: 1, backgroundColor: hex.surfaceAlt },
45
+ h(Text, { color: hex.blue, bold: true, backgroundColor: hex.surfaceAlt }, sym.box.v + ' ' + m._provider.toUpperCase()),
46
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' '.repeat(Math.max(0, w - m._provider.length - 6)) + sym.box.v)
47
+ );
48
+ }
49
+ const isSel = i === idx;
50
+ return h(Box, {
51
+ key: m.id, height: 1,
52
+ backgroundColor: isSel ? hex.selectionBg : hex.bg,
53
+ },
54
+ h(Text, {
55
+ color: isSel ? hex.selectionText : hex.text,
56
+ bold: isSel,
57
+ backgroundColor: isSel ? hex.selectionBg : hex.bg,
58
+ }, (isSel ? '\u276F ' : ' ') + m.label + (m.badge ? ' [' + m.badge + ']' : ''))
59
+ );
60
+ });
61
+
62
+ const count = flat.filter(m => !m._header).length;
63
+
64
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.bg, width: w },
43
65
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
44
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + sym.star + ' ' + (search || 'search models...'))
66
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
67
+ sym.box.tl + sym.box.h.repeat(w - 2) + sym.box.tr)
45
68
  ),
46
- flat.map((m, i) => {
47
- if (m._header) {
48
- return h(Box, { key: 'h-' + m._provider, height: 1, backgroundColor: hex.surfaceAlt },
49
- h(Text, { color: hex.blue, bold: true, backgroundColor: hex.surfaceAlt }, ' ' + sym.triD + ' ' + m._provider.toUpperCase())
50
- );
51
- }
52
- const isSel = i === idx;
53
- return h(Box, {
54
- key: m.id, height: 1,
55
- backgroundColor: isSel ? hex.selectionBg : 'transparent',
56
- },
57
- h(Text, {
58
- color: isSel ? hex.selectionText : hex.text,
59
- bold: isSel,
60
- backgroundColor: isSel ? hex.selectionBg : 'transparent',
61
- }, ' ' + m.label + (m.badge ? ' [' + m.badge + ']' : ''))
62
- );
63
- }),
64
69
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
65
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' ' + sym.arrowU + sym.arrowD + ' nav ' + sym.arrowR + ' select Esc close')
66
- )
70
+ h(Text, { color: hex.gold, backgroundColor: hex.surfaceAlt }, sym.box.v + ' Models ' + count + ' available'),
71
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, ' '.repeat(Math.max(0, w - 19)) + sym.box.v)
72
+ ),
73
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
74
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
75
+ sym.box.v + ' ' + (search || '\u2026') + ' '.repeat(Math.max(0, w - 6)) + sym.box.v)
76
+ ),
77
+ h(Box, { flexDirection: 'column', backgroundColor: hex.bg }, ...items),
78
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
79
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
80
+ sym.box.bl + sym.box.h.repeat(w - 2) + sym.box.br)
81
+ ),
82
+ h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
83
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
84
+ ' \u2191\u2193 nav \u2192 select Esc close')
85
+ ),
67
86
  );
68
87
  }
@@ -1,16 +1,19 @@
1
1
  import React from 'react';
2
2
  import { Box, Text } from 'ink';
3
- import { hex, sym } from '../config/theme.js';
3
+ import { hex } from '../config/theme.js';
4
4
  import { getLayout } from '../config/layout.js';
5
5
  const { createElement: h } = React;
6
6
 
7
7
  export function StatusBar({ model, provider, agentMode, thinking }) {
8
8
  const { cols } = getLayout();
9
- const m = model.replace(/^[^/]+\//, '').slice(0, 22);
10
- const left = sym.circle + ' CLARITY ' + sym.midDot + ' ' + m + ' ' + sym.midDot + ' ' + provider;
11
- const right = (agentMode ? 'AGENT' : 'USER') + (thinking ? ' ' + sym.dot : '');
12
- const gap = Math.max(1, cols - left.length - right.length - 4);
9
+ const m = model.replace(/^[^/]+\//, '').slice(0, 20);
10
+ const left = '\u25C9 CLARITY \u00B7 ' + m + ' \u00B7 ' + provider;
11
+ const right = (agentMode ? '\u25C8 AGENT' : '\u25CB USER') + (thinking ? ' \u25CF' : '');
12
+ const gap = Math.max(1, cols - left.length - right.length - 2);
13
+
13
14
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
14
- h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' ' + left + ' '.repeat(gap) + right + ' ')
15
+ h(Text, { color: hex.accent, bold: true, backgroundColor: hex.surfaceAlt }, ' ' + left),
16
+ h(Text, { backgroundColor: hex.surfaceAlt }, ' '.repeat(gap)),
17
+ h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, right + ' ')
15
18
  );
16
19
  }
@@ -12,40 +12,23 @@ export function ThinkingBlock({ toolResults, duration }) {
12
12
  ? (duration < 1000 ? duration + 'ms' : (duration / 1000).toFixed(1) + 's')
13
13
  : '';
14
14
 
15
- const headerText = sym.triR + ' ' + (collapsed ? sym.triR : sym.triD) + ' Thought' + (durStr ? ' (' + durStr + ')' : '');
16
-
17
- const rows = 1;
18
- const totalRows = collapsed ? 1 : 1 + items.length;
19
-
20
15
  return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
21
16
  h(Box, { height: 1 },
22
- h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + headerText)
17
+ h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt },
18
+ ' ' + (collapsed ? sym.triR : sym.triD) + ' Thought' + (durStr ? ' (' + durStr + ')' : ''))
23
19
  ),
24
- collapsed
25
- ? null
26
- : h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
27
- items.map((tr, i) => {
28
- const isLast = i === items.length - 1;
29
- const prefix = isLast ? sym.treeTip + sym.u.h : sym.treeFork + sym.u.h;
30
- const conn = isLast ? ' ' : sym.treeCon;
31
- const icon = tr.status === 'failed' ? sym.cross : sym.circle;
32
- const col = tr.status === 'failed' ? hex.red : hex.green;
33
- const td = tr.duration ? ' ' + tr.duration + 'ms' : '';
34
- const line = ' ' + prefix + ' ' + icon + ' ' + tr.name + td;
35
- const contentLine = tr.content && tr.content.length < 200
36
- ? ' ' + conn + ' ' + String(tr.content).slice(0, cols - 10)
37
- : null;
38
- return h(Box, { key: tr.execId || i, flexDirection: 'column' },
39
- h(Box, { height: 1 },
40
- h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt }, line)
41
- ),
42
- contentLine
43
- ? h(Box, { height: 1 },
44
- h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, contentLine)
45
- )
46
- : null
47
- );
48
- })
49
- )
20
+ collapsed ? null : h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
21
+ items.map((tr, i) => {
22
+ const isLast = i === items.length - 1;
23
+ const prefix = isLast ? sym.treeTip + '\u2500' : sym.treeFork + '\u2500';
24
+ const conn = isLast ? ' ' : sym.treeCon;
25
+ const ico = tr.status === 'failed' ? sym.cross : sym.circle;
26
+ const td = tr.duration ? ' ' + tr.duration + 'ms' : '';
27
+ return h(Box, { key: tr.execId || i, height: 1 },
28
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
29
+ ' ' + prefix + ' ' + ico + ' ' + tr.name + td)
30
+ );
31
+ })
32
+ )
50
33
  );
51
34
  }
@@ -16,21 +16,25 @@ export function ToolCard({ exec, isActive }) {
16
16
  const c = status === 'failed' ? hex.red : hex.green;
17
17
  const icon = status === 'failed' ? sym.cross : sym.bullet;
18
18
  return h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
19
- h(Text, { color: c, backgroundColor: hex.surfaceAlt }, ' ' + icon + ' ' + name + (dur ? ' ' + dur : ''))
19
+ h(Text, { color: c, backgroundColor: hex.surfaceAlt }, ' ' + icon + ' ' + name + (dur ? ' ' + dur : ''))
20
20
  );
21
21
  }
22
22
 
23
- const w = Math.min(cols - 6, 56);
24
- const lines = [sym.bullet + ' ' + name + (dur ? ' ' + dur : '')];
25
- if (args.length < 80) lines.push(' ' + sym.triR + ' ' + args.slice(0, w));
26
- if (status === 'running') lines.push(' ' + sym.dot + ' running');
27
- if (status === 'failed' && exec.error) lines.push(' ' + sym.cross + ' ' + String(exec.error).slice(0, w - 4));
23
+ const w = Math.min(cols - 8, 56);
28
24
 
29
25
  return h(Box, { flexDirection: 'column', backgroundColor: hex.surfaceAlt },
30
- lines.slice(0, 4).map((line, i) =>
31
- h(Box, { key: i, height: 1 },
32
- h(Text, { color: i === 0 ? hex.purple : hex.textDim, backgroundColor: hex.surfaceAlt }, ' ' + line)
33
- )
34
- )
26
+ h(Box, { height: 1 },
27
+ h(Text, { color: hex.purple, backgroundColor: hex.surfaceAlt }, ' ' + sym.bullet + ' ' + name + (dur ? ' ' + dur : ''))
28
+ ),
29
+ args.length < 80
30
+ ? h(Box, { height: 1 },
31
+ h(Text, { color: hex.textDim, backgroundColor: hex.surfaceAlt }, ' ' + sym.triR + ' ' + args.slice(0, w))
32
+ )
33
+ : null,
34
+ status === 'running'
35
+ ? h(Box, { height: 1 },
36
+ h(Text, { color: hex.blue, backgroundColor: hex.surfaceAlt }, ' ' + sym.dot + ' running')
37
+ )
38
+ : null,
35
39
  );
36
40
  }
@@ -1,17 +1,18 @@
1
1
  import wrapAnsi from 'wrap-ansi';
2
+ import stringWidth from 'string-width';
2
3
 
3
4
  export function getLayout() {
4
5
  const rows = process.stdout.rows || 30;
5
6
  const cols = process.stdout.columns || 80;
6
- return {
7
- rows,
8
- cols,
9
- topBar: 1,
10
- dock: 4,
11
- viewport: Math.max(8, rows - 5),
12
- contentWidth: Math.max(20, cols - 4),
13
- padLeft: ' '.repeat(2),
14
- };
7
+ const headerHeight = 1;
8
+ const dockHeight = 4;
9
+ const viewport = Math.max(8, rows - headerHeight - dockHeight);
10
+ const contentWidth = Math.max(20, cols - 4);
11
+ return { rows, cols, headerHeight, dockHeight, viewport, contentWidth, pad: ' ' };
12
+ }
13
+
14
+ export function sw(text) {
15
+ return stringWidth(text || '');
15
16
  }
16
17
 
17
18
  export function wrapText(text, width) {
@@ -33,9 +34,9 @@ export function truncateText(text, maxLines, width) {
33
34
 
34
35
  export function measureEntry(entry, w) {
35
36
  const nr = entry.role;
36
- if (nr === 'user') return 1 + countLines(entry.content, w) + 0;
37
+ if (nr === 'user') return 1 + countLines(entry.content, w);
37
38
  if (nr === 'assistant') return 2 + countLines(entry.content, w) + (entry.duration ? 1 : 0);
38
- if (nr === 'tool') return 2 + (entry.completed ? 0 : Math.min(6, countLines(entry.content || '', w) + 2));
39
+ if (nr === 'tool') return 1;
39
40
  if (nr === 'system' || nr === 'error') return 1;
40
41
  if (nr === 'streaming') return 2 + Math.min(40, countLines(entry.content, w));
41
42
  return 1;
@@ -78,7 +79,6 @@ export function buildLineArray(slice, clipIndex, clipLines, w) {
78
79
  }
79
80
  } else if (nr === 'assistant') {
80
81
  lines.push({ type: 'asst_head', data: e });
81
- lines.push({ type: 'asst_bar', data: e });
82
82
  if (skip <= 0) {
83
83
  const wrapped = wrapText(e.content, w);
84
84
  const contentLines = wrapped.split('\n');
@@ -94,32 +94,18 @@ export function buildLineArray(slice, clipIndex, clipLines, w) {
94
94
  }
95
95
  if (e.duration) lines.push({ type: 'asst_foot', text: String(e.duration), data: e });
96
96
  } else if (nr === 'tool') {
97
- const label = (e.error ? '\u2716 ' : '\u25C9 ') + (e.toolName || 'tool');
98
- if (e.completed) {
99
- if (skip <= 0) lines.push({ type: 'tool_line', text: label + ' ' + (e.duration ? e.duration + 'ms' : ''), data: e });
100
- } else {
101
- lines.push({ type: 'tool_head', text: label, data: e });
102
- if (e.content) {
103
- const wrapped = wrapText(String(e.content).slice(0, w * 3), w);
104
- const cls = wrapped.split('\n');
105
- for (let ci = Math.max(0, skip - 1); ci < Math.min(cls.length, 4); ci++) {
106
- lines.push({ type: 'tool_line', text: cls[ci], data: e });
107
- }
108
- }
109
- }
97
+ lines.push({ type: 'tool_line', text: (e.error ? '\u2716 ' : '\u25C9 ') + (e.toolName || 'tool') + (e.duration ? ' ' + e.duration + 'ms' : ''), data: e });
110
98
  } else if (nr === 'system') {
111
99
  if (skip <= 0) lines.push({ type: 'sys_line', text: e.content, data: e });
112
100
  } else if (nr === 'error') {
113
101
  if (skip <= 0) lines.push({ type: 'err_line', text: e.content, data: e });
114
102
  } else if (nr === 'streaming') {
115
103
  lines.push({ type: 'stream_head', data: e });
116
- lines.push({ type: 'stream_bar', data: e });
117
104
  if (e.status) lines.push({ type: 'stream_status', text: e.status, data: e });
118
105
  const wrapped = wrapText(e.content || '', w);
119
106
  const contentLines = wrapped.split('\n');
120
- const maxLines = 40;
121
107
  const startLine = Math.min(skip, contentLines.length);
122
- const endLine = Math.min(contentLines.length, startLine + (maxLines - lines.length));
108
+ const endLine = Math.min(contentLines.length, startLine + 40);
123
109
  for (let ci = startLine; ci < endLine; ci++) {
124
110
  lines.push({ type: 'stream_line', text: contentLines[ci], data: e });
125
111
  }
@@ -1,6 +1,6 @@
1
1
  export const ALL_MODELS = [
2
- { id: 'huggingface/Universal-618/Clarity-flash-weights', provider: 'huggingface', label: 'Clarity Flash (Qwen 2.5 1.5B)', badge: 'Free' },
3
- { id: 'huggingface/Universal-618/Clarity-heavy-weights', provider: 'huggingface', label: 'Clarity Heavy (Qwen 2.5 1.5B)', badge: 'Free' },
2
+ { id: 'huggingface/Universal-618/Clarity-flash-weights', provider: 'huggingface', label: 'DeepSeek R1 1.5B', badge: 'Reasoning' },
3
+ { id: 'huggingface/Universal-618/Clarity-heavy-weights', provider: 'huggingface', label: 'DeepSeek R1 7B', badge: 'Reasoning' },
4
4
  { id: 'groq/llama-3.3-70b-versatile', provider: 'groq', label: 'Llama 3.3 70B Versatile', badge: null },
5
5
  { id: 'groq/llama-3.1-8b-instant', provider: 'groq', label: 'Llama 3.1 8B Instant', badge: 'Fast' },
6
6
  { id: 'groq/llama-4-scout-17b-16e-instruct', provider: 'groq', label: 'Llama 4 Scout 17B', badge: null },
@@ -1,4 +1,6 @@
1
1
  import chalk from 'chalk';
2
+ import gradient from 'gradient-string';
3
+ import figures from 'figures';
2
4
 
3
5
  export const hex = {
4
6
  bg: '#0A0A14',
@@ -21,7 +23,6 @@ export const hex = {
21
23
  textMuted: '#555577',
22
24
  white: '#FFFFFF',
23
25
  black: '#000000',
24
- modalOverlay: 'rgba(0,0,0,0.85)',
25
26
  };
26
27
 
27
28
  export const color = {
@@ -46,32 +47,47 @@ export const color = {
46
47
  };
47
48
 
48
49
  export const sym = {
49
- diamond: '\u25C6',
50
50
  circle: '\u25C9',
51
51
  dot: '\u25CF',
52
- smallDot: '\u25CB',
53
52
  triR: '\u25B8',
54
53
  triD: '\u25BE',
55
54
  bullet: '\u25C9',
56
55
  cross: '\u2716',
57
56
  ellipsis: '\u2026',
58
57
  mdash: '\u2014',
59
- ndash: '\u2013',
60
58
  midDot: '\u00B7',
61
59
  arrowR: '\u2192',
62
- arrowL: '\u2190',
63
60
  arrowU: '\u2191',
64
61
  arrowD: '\u2193',
65
- lightV: '\u2502',
66
- lightH: '\u2500',
62
+ box: {
63
+ tl: '\u250C', tr: '\u2510', bl: '\u2514', br: '\u2518',
64
+ h: '\u2500', v: '\u2502',
65
+ },
67
66
  treeJ: '\u2514',
68
67
  treeT: '\u251C',
69
68
  treeCon: '\u2502',
70
- triR2: '\u25B8',
71
- triD2: '\u25BE',
72
- u: { h: '\u2500' },
73
69
  treeTip: '\u2570',
74
70
  treeFork: '\u256D',
75
71
  star: '\u2726',
76
- asterisk: '\u2731',
72
+ gear: '\u2699',
73
+ pointer: '\u276F',
77
74
  };
75
+
76
+ export const gradientText = gradient(['#FF6B35', '#3B82F6']);
77
+ export const gradientLine = gradient(['#FF6B35', '#3B82F6']);
78
+
79
+ export const LOGO = [
80
+ ' ',
81
+ ' ██████ ██ █████ ██████ ██ ████████ ██ ██ ',
82
+ ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
83
+ ' ██████ ██ ███████ ██████ ██ ██ ██ ██ ',
84
+ ' ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
85
+ ' ██ ███████ ██ ██ ██ ██ ██ ██ ██ ██████▄ ',
86
+ ' ',
87
+ ' █████ ██ ',
88
+ ' ██ ██ ██ ',
89
+ ' ██████ ██ ',
90
+ ' ██ ██ ██ ',
91
+ ' ██ ██ ███████ ',
92
+ ' ',
93
+ ];