clarity-ai 7.2.0 → 7.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ---
4
4
 
5
+ ## 7.2.1 (2026-06-06)
6
+
7
+ ### Critical hotfix: mouse bleed elimination, CLARITY branding, TrueColor textures
8
+ - **FIXED input corruption**: Removed XTerm mouse tracking (`\x1b[?1000h`) entirely. Raw `\x1b[<0;54;41M` sequences no longer leak into the chat text buffer. Termux soft keyboard now works reliably without raw-mode conflicts.
9
+ - **FIXED ASCII logo**: Replaced the figlet banner that rendered as "GLARITY" with a clean 5-line solid block matrix (`██████`) that unambiguously spells CLARITY. Three-tier sizing (5-line / 3-line compact / 1-line gradient).
10
+ - **TrueColor panel textures**: Every message block, streaming pane, and popup container now uses explicit `#1C1C1C` graphite backgrounds. No naked terminal defaults. Orange `#FF9F43` full-width selection bar with `#000000` black text on all active popup rows.
11
+ - **Stderr isolation maintained**: `console.log/error/warn` continue routing to `clarity-debug.log` to prevent stray backend traces from corrupting the UI layer.
12
+
5
13
  ## 7.2.0 (2026-06-06)
6
14
 
7
15
  ### Sticky floating overlay engine — touch selection, phase-driven layout, virtualized log
package/bin/clarity.js CHANGED
@@ -57,14 +57,14 @@ async function main() {
57
57
  logFile.write('[CLEANUP] CLARITY exiting\n');
58
58
  logFile.end();
59
59
  try { clear(); } catch {}
60
- process.stdout.write('\x1b[?1000l\x1b[?1006l\x1b[?25h\x1b[0m');
60
+ process.stdout.write('\x1b[?25h\x1b[0m');
61
61
  process.exit(0);
62
62
  }
63
63
 
64
64
  process.on('SIGINT', () => cleanup());
65
65
  process.on('SIGTERM', () => cleanup());
66
66
  process.on('exit', () => {
67
- process.stdout.write('\x1b[?1000l\x1b[?1006l\x1b[?25h\x1b[0m');
67
+ process.stdout.write('\x1b[?25h\x1b[0m');
68
68
  });
69
69
 
70
70
  await new Promise(() => {});
@@ -76,7 +76,7 @@ main().catch(err => {
76
76
  console.warn = originalWarn;
77
77
  logFile.write('[FATAL] ' + (err?.message || String(err)) + '\n');
78
78
  logFile.end();
79
- process.stdout.write('\x1b[?1000l\x1b[?1006l\x1b[?25h\x1b[0m');
79
+ process.stdout.write('\x1b[?25h\x1b[0m');
80
80
  console.error('\n\x1b[31mFatal error:\x1b[0m', err?.message || err);
81
81
  process.exit(1);
82
82
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clarity-ai",
3
- "version": "7.2.0",
3
+ "version": "7.2.1",
4
4
  "description": "CLARITY — terminal AI agent with local GGUF inference on HF Spaces",
5
5
  "type": "module",
6
6
  "bin": {
package/src/app.js CHANGED
@@ -8,7 +8,6 @@ import { StreamView } from './components/StreamView.js';
8
8
  import { InputDock } from './components/InputDock.js';
9
9
  import { SlashPopup } from './components/SlashPopup.js';
10
10
  import { Footer } from './components/Footer.js';
11
- import { useMouse } from './hooks/useMouse.js';
12
11
  const { createElement: h } = React;
13
12
 
14
13
  let abortController = null;
@@ -24,6 +23,12 @@ const FOOTER_H = 2;
24
23
  const STATUS_H = 1;
25
24
  const POPUP_W = 44;
26
25
 
26
+ const COMMAND_ITEMS = [
27
+ { name: '/keys' }, { name: '/model' }, { name: '/provider' },
28
+ { name: '/agent' }, { name: '/stop' }, { name: '/clear' },
29
+ { name: '/export' }, { name: '/help' }, { name: '/exit' },
30
+ ];
31
+
27
32
  function deriveStatus(state, sc) {
28
33
  if (!state.thinking) return 'idle';
29
34
  return sc ? 'streaming' : 'thinking';
@@ -33,6 +38,12 @@ function hasRealMessages(msgs) {
33
38
  return msgs.some(m => m.role === 'user' || m.role === 'assistant');
34
39
  }
35
40
 
41
+ function getFiltered(input) {
42
+ const q = input.replace(/^\//, '').toLowerCase();
43
+ if (!q) return COMMAND_ITEMS;
44
+ return COMMAND_ITEMS.filter(c => c.name.includes(q));
45
+ }
46
+
36
47
  export function App({ config }) {
37
48
  const { stdout } = useStdout();
38
49
  const [dims, setDims] = useState({ rows: stdout.rows || 30, cols: stdout.columns || 80 });
@@ -54,7 +65,7 @@ export function App({ config }) {
54
65
 
55
66
  const rows = dims.rows;
56
67
  const cols = dims.cols;
57
- const bannerH = cols < 50 ? 2 : 6;
68
+ const bannerH = cols < 50 ? 2 : (cols < 60 ? 3 : 5);
58
69
  const status = deriveStatus(state, streamContent);
59
70
  const isChat = hasRealMessages(state.messages);
60
71
  const cardW = Math.min(cols - 4, 56);
@@ -62,7 +73,7 @@ export function App({ config }) {
62
73
  const availLines = Math.max(2, rows - bannerH - STATUS_H - DOCK_H - FOOTER_H - 2);
63
74
 
64
75
  useEffect(() => {
65
- function onResize() { setDims({ r: process.stdout.rows || 30, c: process.stdout.columns || 80 }); }
76
+ function onResize() { setDims({ rows: process.stdout.rows || 30, cols: process.stdout.columns || 80 }); }
66
77
  process.stdout.on('resize', onResize);
67
78
  return () => process.stdout.removeListener('resize', onResize);
68
79
  }, []);
@@ -85,8 +96,8 @@ export function App({ config }) {
85
96
  }, []);
86
97
 
87
98
  const onCommand = useCallback(async (val) => {
88
- if (val.startsWith('/stop')) { cancelStream(); return; }
89
- if (val.startsWith('/exit')) { process.exit(0); return; }
99
+ if (val === '/stop') { cancelStream(); return; }
100
+ if (val === '/exit') { process.exit(0); return; }
90
101
  await handleCommand(val, stateRef.current, setState, setModel, setProvider, modelRef.current, providerRef.current);
91
102
  }, []);
92
103
 
@@ -108,7 +119,6 @@ export function App({ config }) {
108
119
  setInput('');
109
120
  setPopupIdx(0);
110
121
  if (cmd === '/exit') process.exit(0);
111
- if (cmd === '/stop') { cancelStream(); return; }
112
122
  onCommand(cmd);
113
123
  }
114
124
 
@@ -118,41 +128,21 @@ export function App({ config }) {
118
128
  if (!showPopup) return;
119
129
  if (key.escape) { closePopup(); return; }
120
130
  if (key.return) {
121
- const COMMANDS = [{ name: '/keys' }, { name: '/model' }, { name: '/provider' }, { name: '/agent' }, { name: '/stop' }, { name: '/clear' }, { name: '/export' }, { name: '/help' }, { name: '/exit' }];
122
- const q = input.replace(/^\//, '').toLowerCase();
123
- const filtered = q ? COMMANDS.filter(c => c.name.includes(q)) : COMMANDS;
131
+ const filtered = getFiltered(input);
124
132
  if (filtered[popupIdx]) handlePopupSelect(filtered[popupIdx].name);
125
133
  return;
126
134
  }
127
135
  if (key.upArrow) { setPopupIdx(i => Math.max(0, i - 1)); return; }
128
136
  if (key.downArrow) {
129
- const COMMANDS = [{ name: '/keys' }, { name: '/model' }, { name: '/provider' }, { name: '/agent' }, { name: '/stop' }, { name: '/clear' }, { name: '/export' }, { name: '/help' }, { name: '/exit' }];
130
- const q = input.replace(/^\//, '').toLowerCase();
131
- const filtered = q ? COMMANDS.filter(c => c.name.includes(q)) : COMMANDS;
137
+ const filtered = getFiltered(input);
132
138
  setPopupIdx(i => Math.min(filtered.length - 1, i + 1));
133
139
  }
134
140
  });
135
141
 
136
- const handleClick = useCallback(({ col, row }) => {
137
- if (!showPopup) return;
138
- const popupLeft = Math.floor((cols - POPUP_W) / 2) + 1;
139
- const popupH = 1 + 9 + 1;
140
- const popupTop = rows - DOCK_H - FOOTER_H - popupH;
141
- if (col >= popupLeft && col < popupLeft + POPUP_W && row >= popupTop && row < popupTop + popupH) {
142
- const itemRow = row - popupTop - 1;
143
- const COMMANDS = [{ name: '/keys' }, { name: '/model' }, { name: '/provider' }, { name: '/agent' }, { name: '/stop' }, { name: '/clear' }, { name: '/export' }, { name: '/help' }, { name: '/exit' }];
144
- if (itemRow >= 0 && itemRow < COMMANDS.length) {
145
- handlePopupSelect(COMMANDS[itemRow].name);
146
- }
147
- }
148
- }, [showPopup, cols, rows]);
149
-
150
- useMouse(handleClick);
151
-
152
142
  const bottomArea = h(Box, { flexDirection: 'column', alignItems: 'center', width: '100%' },
153
143
  showPopup
154
144
  ? h(Box, { position: 'absolute', bottom: DOCK_H + FOOTER_H, alignItems: 'center', width: '100%' },
155
- h(SlashPopup, { search: input, selectedIdx: popupIdx, onHover: setPopupIdx, width: POPUP_W })
145
+ h(SlashPopup, { search: input, selectedIdx: popupIdx, width: POPUP_W })
156
146
  )
157
147
  : null,
158
148
  h(InputDock, {
@@ -3,22 +3,18 @@ import { Box, Text } from 'ink';
3
3
  import { appGradient } from '../config/theme.js';
4
4
  const { createElement: h } = React;
5
5
 
6
- const BANNER_FULL = [
7
- ' ██████╗██╗ █████╗ ██████╗ ██╗████████╗██╗ ██╗',
8
- '██╔════╝██║ ██╔══██╗██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝',
9
- '██║ ███╗██║ ███████║██████╔╝██║ ██║ ╚████╔╝ ',
10
- '██║ ██║██║ ██╔══██║██╔══██╗██║ ██║ ╚██╔╝ ',
11
- '╚██████╔╝███████╗██║ ██║██║ ██║██║ ██║ ██║ ',
12
- ' ╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ',
6
+ const BANNER = [
7
+ '██████ ██ █████ ██████ ██ ████████ ██ ██',
8
+ '██ ██ ██ ██ ██ ██ ██ ██ ██ ██',
9
+ '██ ██ ███████ ██████ ██ ██ ████',
10
+ '██ ██ ██ ██ ██ ██ ██ ██ ██',
11
+ '██████ ███████ ██ ██ ██ ██ ██ ██ ██',
13
12
  ];
14
13
 
15
- const BANNER_MED = [
16
- '██████╗ ██╗ █████╗ ██████╗ ██╗████████╗██╗ ██╗',
17
- '██╔════╝ ██║ ██╔══██╗██╔══██╗██║╚══██╔══╝╚██╗ ██╔╝',
18
- '███████╗ ██║ ███████║██████╔╝██║ ██║ ╚████╔╝ ',
19
- '╚════██║ ██║ ██╔══██║██╔══██╗██║ ██║ ╚██╔╝ ',
20
- '██████╔╝ ███████╗██║ ██║██║ ██║██║ ██║ ██║ ',
21
- '╚═════╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ',
14
+ const BANNER_COMPACT = [
15
+ '██████ ██ █████ ██████ ██ ████████ ██ ██',
16
+ '██ ██ ██ ██ ██ ██ ██ ██ ██ ██',
17
+ '██████ ███████ ██ ██ ██ ██ ██ ██ ██',
22
18
  ];
23
19
 
24
20
  export function Banner({ cols }) {
@@ -28,10 +24,9 @@ export function Banner({ cols }) {
28
24
  );
29
25
  }
30
26
 
31
- const lines = cols < 60 ? BANNER_MED : BANNER_FULL;
32
- const bannerHeight = lines.length;
27
+ const lines = cols < 60 ? BANNER_COMPACT : BANNER;
33
28
 
34
- return h(Box, { height: bannerHeight, width: '100%', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' },
29
+ return h(Box, { height: lines.length, width: '100%', alignItems: 'center', justifyContent: 'center', flexDirection: 'column' },
35
30
  lines.map((line, i) =>
36
31
  h(Box, { key: i, height: 1 },
37
32
  h(Text, { bold: true }, appGradient.multiline(line))
@@ -1,4 +1,4 @@
1
- import React, { useState, useMemo } from 'react';
1
+ import React, { useMemo } from 'react';
2
2
  import { Box, Text } from 'ink';
3
3
  import { hex } from '../config/theme.js';
4
4
  const { createElement: h } = React;
@@ -15,7 +15,7 @@ const COMMANDS = [
15
15
  { name: '/exit', desc: 'Exit CLARITY' },
16
16
  ];
17
17
 
18
- export function SlashPopup({ search, selectedIdx, onHover, width }) {
18
+ export function SlashPopup({ search, selectedIdx, width }) {
19
19
  const filtered = useMemo(() => {
20
20
  const q = search.replace(/^\//, '').toLowerCase();
21
21
  if (!q) return COMMANDS;
@@ -25,31 +25,33 @@ export function SlashPopup({ search, selectedIdx, onHover, width }) {
25
25
  }, [search]);
26
26
 
27
27
  const innerW = Math.max(10, width - 4);
28
- const itemLabel = (cmd, i) => {
28
+ const label = (cmd, i) => {
29
29
  const sel = i === selectedIdx;
30
- const label = ' ' + cmd.name + ' ' + cmd.desc;
31
- return label.length > innerW ? label.slice(0, innerW - 2) + '\u2026' : label;
30
+ const text = ' ' + cmd.name + ' ' + cmd.desc;
31
+ return text.length > innerW ? text.slice(0, innerW - 2) + '\u2026' : text;
32
32
  };
33
33
 
34
- return h(Box, { flexDirection: 'column', width, backgroundColor: hex.surface },
34
+ return h(Box, { flexDirection: 'column', width, backgroundColor: hex.cardBg },
35
35
  h(Box, { height: 1, backgroundColor: hex.surfaceAlt },
36
- h(Text, { color: hex.textMuted }, ' ' + (search || '/') + ' '.repeat(Math.max(0, innerW - (search || '/').length)))
36
+ h(Text, { color: hex.textMuted, backgroundColor: hex.surfaceAlt },
37
+ ' ' + (search || '/') + ' '.repeat(Math.max(0, innerW - (search || '/').length)))
37
38
  ),
38
39
  filtered.map((cmd, i) =>
39
40
  h(Box, {
40
41
  key: cmd.name,
41
42
  height: 1,
42
- backgroundColor: i === selectedIdx ? hex.orange : 'transparent',
43
+ backgroundColor: i === selectedIdx ? hex.orange : hex.cardBg,
43
44
  },
44
45
  h(Text, {
45
46
  color: i === selectedIdx ? '#000000' : hex.text,
46
47
  bold: i === selectedIdx,
47
- }, itemLabel(cmd, i))
48
+ backgroundColor: i === selectedIdx ? hex.orange : hex.cardBg,
49
+ }, label(cmd, i))
48
50
  )
49
51
  ),
50
52
  filtered.length === 0
51
- ? h(Box, { height: 1 },
52
- h(Text, { color: hex.textMuted }, ' No matching commands'))
53
+ ? h(Box, { height: 1, backgroundColor: hex.cardBg },
54
+ h(Text, { color: hex.textMuted, backgroundColor: hex.cardBg }, ' No matching commands'))
53
55
  : null,
54
56
  );
55
57
  }
@@ -23,7 +23,7 @@ function wrap(text, width) {
23
23
  return wrapAnsi(String(text), Math.max(4, width), { trim: false, hard: true }).split('\n');
24
24
  }
25
25
 
26
- function MsgRole({ role }) {
26
+ function RoleIcon({ role }) {
27
27
  switch (role) {
28
28
  case 'user': return h(Text, { color: hex.orange, bold: true }, '\u276F ');
29
29
  case 'assistant': return h(Text, { color: hex.purple, bold: true }, '\u25C6 ');
@@ -36,20 +36,21 @@ function MsgRole({ role }) {
36
36
 
37
37
  function MsgBlock({ msg, width }) {
38
38
  const lines = useMemo(() => wrap(msg.content, width), [msg.content, width]);
39
+ const bg = msg.role === 'user' ? hex.surface : hex.cardBg;
39
40
 
40
- return h(Box, { flexDirection: 'column' },
41
- h(Box, { height: 1 },
42
- h(MsgRole, { role: msg.role }),
43
- h(Text, { color: hex.text }, lines[0] || '')
41
+ return h(Box, { flexDirection: 'column', backgroundColor: bg },
42
+ h(Box, { height: 1, backgroundColor: bg },
43
+ h(RoleIcon, { role: msg.role }),
44
+ h(Text, { color: hex.text, backgroundColor: bg }, lines[0] || '')
44
45
  ),
45
46
  lines.slice(1).map((l, i) =>
46
- h(Box, { key: i, height: 1 },
47
- h(Text, { color: hex.text }, ' ' + l)
47
+ h(Box, { key: i, height: 1, backgroundColor: bg },
48
+ h(Text, { color: hex.text, backgroundColor: bg }, ' ' + l)
48
49
  )
49
50
  ),
50
51
  msg.role === 'assistant' && msg.duration
51
- ? h(Box, { height: 1 },
52
- h(Text, { color: hex.textMuted },
52
+ ? h(Box, { height: 1, backgroundColor: bg },
53
+ h(Text, { color: hex.textMuted, backgroundColor: bg },
53
54
  ' ' + (msg.duration < 1000 ? msg.duration + 'ms' : (msg.duration / 1000).toFixed(1) + 's'))
54
55
  )
55
56
  : null
@@ -58,14 +59,14 @@ function MsgBlock({ msg, width }) {
58
59
 
59
60
  function StreamingBlock({ content, width }) {
60
61
  const lines = useMemo(() => wrap(content, width), [content, width]);
61
- return h(Box, { flexDirection: 'column' },
62
- h(Box, { height: 1 },
63
- h(Text, { color: hex.blue, bold: true }, '\u25CF '),
64
- h(Text, { color: hex.text }, lines[0] || '')
62
+ return h(Box, { flexDirection: 'column', backgroundColor: hex.cardBg },
63
+ h(Box, { height: 1, backgroundColor: hex.cardBg },
64
+ h(Text, { color: hex.blue, bold: true, backgroundColor: hex.cardBg }, '\u25CF '),
65
+ h(Text, { color: hex.text, backgroundColor: hex.cardBg }, lines[0] || '')
65
66
  ),
66
67
  lines.slice(1).map((l, i) =>
67
- h(Box, { key: i, height: 1 },
68
- h(Text, { color: hex.text }, ' ' + l)
68
+ h(Box, { key: i, height: 1, backgroundColor: hex.cardBg },
69
+ h(Text, { color: hex.text, backgroundColor: hex.cardBg }, ' ' + l)
69
70
  )
70
71
  )
71
72
  );
@@ -110,8 +111,8 @@ export function StreamView({ messages, streamContent, status, maxLines, width })
110
111
  ? h(StreamingBlock, { content: streamContent, width: cw })
111
112
  : null,
112
113
  status === 'thinking' && !streamContent
113
- ? h(Box, { height: 1 },
114
- h(Text, { color: hex.textMuted }, ' \u25CF processing...')
114
+ ? h(Box, { height: 1, backgroundColor: hex.cardBg },
115
+ h(Text, { color: hex.textMuted, backgroundColor: hex.cardBg }, ' \u25CF processing...')
115
116
  )
116
117
  : null,
117
118
  );
@@ -1,45 +0,0 @@
1
- import { useEffect, useRef } from 'react';
2
- import { useStdin } from 'ink';
3
-
4
- export function useMouse(handler) {
5
- const { stdin } = useStdin();
6
- const handlerRef = useRef(handler);
7
- handlerRef.current = handler;
8
-
9
- useEffect(() => {
10
- process.stdout.write('\x1b[?1000h\x1b[?1006h');
11
-
12
- function onData(chunk) {
13
- const str = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
14
-
15
- const sgr = str.match(/\x1b\[<(\d+);(\d+);(\d+)([Mm])/);
16
- if (sgr) {
17
- const code = parseInt(sgr[1]);
18
- const col = parseInt(sgr[2]);
19
- const row = parseInt(sgr[3]);
20
- const press = sgr[4] === 'M';
21
- if (press && handlerRef.current) {
22
- handlerRef.current({ col, row, button: code & 3 });
23
- }
24
- return;
25
- }
26
-
27
- const legacy = str.match(/\x1b\[M(.{3})/);
28
- if (legacy) {
29
- const chars = legacy[1];
30
- const cb = chars.charCodeAt(0) - 32;
31
- const col = chars.charCodeAt(1) - 32;
32
- const row = chars.charCodeAt(2) - 32;
33
- if (handlerRef.current) {
34
- handlerRef.current({ col, row, button: cb & 3 });
35
- }
36
- }
37
- }
38
-
39
- stdin.on('data', onData);
40
- return () => {
41
- stdin.removeListener('data', onData);
42
- process.stdout.write('\x1b[?1000l\x1b[?1006l');
43
- };
44
- }, [stdin]);
45
- }