flowmind 1.4.8 → 1.5.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.
@@ -1,92 +1,23 @@
1
1
  const React = require('react');
2
2
  const { Box, Text } = require('ink');
3
+ const { LEVEL_COLORS, LEVEL_NAMES, LEVEL_STATES, getDragonArt } = require('../ui');
3
4
 
4
- const DRAGON_ARTS = {
5
- 0: [
6
- ' ╭─────╮ ',
7
- ' ╱ ╭─╮ ╲ ',
8
- ' │ │ │ │ ',
9
- ' │ │ ◎ │ │ ',
10
- ' │ ╰─╯ │ ',
11
- ' ╲ ╱ ',
12
- ' ╰─────╯ ',
13
- ],
14
- 1: [
15
- ' ╭──╮ ',
16
- ' ╭────╯ ╰───╮ ',
17
- ' ╱ ◎ ╰─╯ ╲ ',
18
- ' ╱ ▽ ╲ ',
19
- ' ╲ ╱╲ ╱╲ ╱ ',
20
- ' ╲╱╱ ╲╱╱ ╲╱╲╱ ',
21
- ],
22
- 2: [
23
- ' ╭─╮ ╭─╮ ',
24
- ' ╭────╯ ╰──╯ ╰───╮ ',
25
- ' ╱ ◎ ╰──╯ ╲ ',
26
- ' ╱ ╭────────╮ ╲ ',
27
- ' ╲ ╱ ╱╱╱╱╱╱╱╱ ╲ ╱ ',
28
- ' ╲───╯ ╱╱╱╱╱╱╱╱╱╱ ╰──╱ ',
29
- ' ╰─╯ ╰─╯ ',
30
- ],
31
- 3: [
32
- ' ╭───╮ ╭───╮ ',
33
- ' ╭───╯ ╰──╯ ╰───╮ ',
34
- ' ╱ ◎ ╰───╯ ╲ ',
35
- '│ ╭──────────╮ │ ',
36
- '│ ╱ ╱╱╱╱╱╱╱╱╱╱ ╲ │ ',
37
- ' ╲──╯ ╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',
38
- ' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╱ ',
39
- ' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',
40
- ' ╰───╯ ╰───╯ ',
41
- ],
42
- 4: [
43
- ' ╭───╮ ╭───╮ ',
44
- '╭───╯ ╰──────╯ ╰───╮ ',
45
- '│ ◎ ╰───╯ │ ',
46
- '│ ╭────────────╮ │ ',
47
- '│ ╱ ╱╱╱╱╱╱╱╱╱╱╱╱ ╲ │ ',
48
- ' ╲───╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰──╯ ',
49
- ' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╲ ',
50
- ' ╲─╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰─╲ ',
51
- ' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',
52
- ' ╰───╯ ╰───╯ ',
53
- ],
54
- 5: [
55
- ' ★ ╭───╮ ╭───╮ ★ ',
56
- '╭─╯ ╰──╯ ╰──╯ ╰─╮ ',
57
- '│ ◎ ╰───╯ │ ',
58
- '│ ╭──────────────╮ │ ',
59
- '│ ╱ ★╱╱╱╱╱╱╱╱╱╱★╱╱ ╲ │ ',
60
- ' ╲────╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',
61
- ' ╲ ╱╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱ ╲ ',
62
- ' ╲──╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰──╲ ',
63
- ' ╲─╯╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱╰──╲ ',
64
- ' ★ ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ★ ',
65
- ' ╰───╯ ╰───╯ ',
66
- ],
67
- };
68
-
69
- const LEVEL_NAMES = ['Egg', 'Hatchling', 'Juvenile', 'Adult', 'Elder', 'Ascended'];
70
- const LEVEL_STATES = ['dormant', 'awakening', 'growing', 'soaring', 'wise', 'transcendent'];
71
- const LEVEL_COLORS = ['gray', 'cyan', 'cyan', 'cyanBright', 'cyanBright', 'cyanBright'];
72
-
73
- function DragonTotem({ honorData, compact }) {
5
+ function DragonTotem({ honorData, compact, asciiMode = false }) {
74
6
  const points = honorData?.points || 0;
75
7
  const level = honorData?.level || 0;
76
- const art = DRAGON_ARTS[level] || DRAGON_ARTS[0];
8
+ const art = getDragonArt(level, { asciiMode, compact });
77
9
  const color = LEVEL_COLORS[level] || 'gray';
78
10
  const levelName = LEVEL_NAMES[level] || 'Unknown';
79
11
  const state = LEVEL_STATES[level] || 'unknown';
80
12
  const nextLevelPoints = [1, 10, 30, 60, 100];
81
13
  const nextPoints = nextLevelPoints[level] || null;
82
14
  const pointsToNext = nextPoints !== null ? nextPoints - points : 0;
83
- const lines = compact ? art.slice(0, 4) : art;
84
15
 
85
16
  return (
86
17
  React.createElement(Box, { flexDirection: 'column', paddingX: 1 },
87
18
  React.createElement(Text, { bold: true, color: 'cyan' }, 'Dragon Totem'),
88
19
  React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
89
- lines.map((line, i) => React.createElement(Text, { key: i, color: color }, line))
20
+ art.map((line, i) => React.createElement(Text, { key: i, color: color }, line))
90
21
  ),
91
22
  React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
92
23
  React.createElement(Text, null,
@@ -1,8 +1,9 @@
1
1
  const React = require('react');
2
2
  const { Box, Text, useInput } = require('ink');
3
3
  const DragonTotem = require('./DragonTotem.jsx');
4
+ const { getBorderStyle, getProgressBar, getSelectionPrefix } = require('../ui');
4
5
 
5
- function Sidebar({ flowmind, width, onSkillSelect, focused }) {
6
+ function Sidebar({ flowmind, width, onSkillSelect, focused, asciiMode = false }) {
6
7
  const [selectedIndex, setSelectedIndex] = React.useState(0);
7
8
  const [skills, setSkills] = React.useState([]);
8
9
  const [honorData, setHonorData] = React.useState({ points: 0, level: 0, stats: {} });
@@ -32,13 +33,12 @@ function Sidebar({ flowmind, width, onSkillSelect, focused }) {
32
33
 
33
34
  const barWidth = Math.max(10, width - 4);
34
35
  const progress = honorData.points > 0 ? Math.min(1, honorData.points / 100) : 0;
35
- const filled = Math.round(progress * barWidth);
36
- const progressBar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled);
36
+ const progressBar = getProgressBar(barWidth, progress, asciiMode);
37
37
 
38
38
  return (
39
- React.createElement(Box, { flexDirection: 'column', width: width, borderStyle: 'single', borderColor: focused ? 'cyan' : 'gray', paddingX: 1 },
39
+ React.createElement(Box, { flexDirection: 'column', width: width, borderStyle: getBorderStyle(asciiMode), borderColor: focused ? 'cyan' : 'gray', paddingX: 1 },
40
40
  React.createElement(Text, { color: focused ? 'cyan' : 'gray' }, focused ? 'Sidebar [Focused]' : 'Sidebar'),
41
- React.createElement(DragonTotem, { honorData: honorData, compact: true }),
41
+ React.createElement(DragonTotem, { honorData: honorData, compact: true, asciiMode: asciiMode }),
42
42
  React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
43
43
  React.createElement(Text, { bold: true, color: 'cyan' }, 'Progress'),
44
44
  React.createElement(Text, { color: 'green' }, progressBar),
@@ -51,7 +51,7 @@ function Sidebar({ flowmind, width, onSkillSelect, focused }) {
51
51
  skills.map((skill, i) => {
52
52
  const isSelected = i === selectedIndex;
53
53
  const category = skill.category || 'general';
54
- const prefix = isSelected ? '\u25B6 ' : ' ';
54
+ const prefix = getSelectionPrefix(isSelected, asciiMode);
55
55
  return React.createElement(Text, { key: skill.name },
56
56
  React.createElement(Text, { color: isSelected ? (focused ? 'green' : 'yellow') : 'white' }, prefix + skill.name),
57
57
  React.createElement(Text, { color: 'gray' }, ' [' + category + ']')
@@ -61,7 +61,7 @@ function Sidebar({ flowmind, width, onSkillSelect, focused }) {
61
61
  ),
62
62
  React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
63
63
  React.createElement(Text, { color: 'gray' }, 'Tab: switch focus'),
64
- React.createElement(Text, { color: 'gray' }, 'Enter: inspect skill')
64
+ React.createElement(Text, { color: 'gray' }, 'Enter: inspect in chat')
65
65
  ),
66
66
  React.createElement(Box, { flexDirection: 'column', marginTop: 1 },
67
67
  React.createElement(Text, { bold: true, color: 'cyan' }, 'Stats'),
@@ -1,9 +1,8 @@
1
1
  const React = require('react');
2
2
  const { Box, Text } = require('ink');
3
+ const { LEVEL_NAMES, getBorderStyle, getStatusDot } = require('../ui');
3
4
 
4
- const LEVEL_NAMES = ['Egg', 'Hatchling', 'Juvenile', 'Adult', 'Elder', 'Ascended'];
5
-
6
- function StatusBar({ flowmind, focusPanel }) {
5
+ function StatusBar({ flowmind, focusPanel, asciiMode = false }) {
7
6
  const [aiStatus, setAiStatus] = React.useState(null);
8
7
  const [componentStatus, setComponentStatus] = React.useState(null);
9
8
  const [honorData, setHonorData] = React.useState(null);
@@ -26,11 +25,11 @@ function StatusBar({ flowmind, focusPanel }) {
26
25
  const points = honorData?.points || 0;
27
26
 
28
27
  return (
29
- React.createElement(Box, { borderStyle: 'single', borderColor: 'gray', paddingX: 1, justifyContent: 'space-between' },
28
+ React.createElement(Box, { borderStyle: getBorderStyle(asciiMode), borderColor: 'gray', paddingX: 1, justifyContent: 'space-between' },
30
29
  React.createElement(Text, null,
31
30
  React.createElement(Text, { color: 'gray' }, 'AI: '),
32
31
  React.createElement(Text, { color: aiOk ? 'green' : 'red' }, aiName),
33
- React.createElement(Text, { color: aiOk ? 'green' : 'red' }, aiOk ? ' \u25CF' : ' \u25CB')
32
+ React.createElement(Text, { color: aiOk ? 'green' : 'red' }, getStatusDot(aiOk, asciiMode))
34
33
  ),
35
34
  React.createElement(Text, null,
36
35
  React.createElement(Text, { color: 'gray' }, 'Components: '),
@@ -45,7 +44,7 @@ function StatusBar({ flowmind, focusPanel }) {
45
44
  React.createElement(Text, null,
46
45
  React.createElement(Text, { color: 'gray' }, 'Focus: '),
47
46
  React.createElement(Text, { color: focusPanel === 'chat' ? 'green' : 'cyan' }, focusPanel || 'chat'),
48
- React.createElement(Text, { color: 'gray' }, ' | Tab switch')
47
+ React.createElement(Text, { color: 'gray' }, ' | Enter send | exit quit :q | Tab switch')
49
48
  )
50
49
  )
51
50
  );
@@ -0,0 +1,164 @@
1
+ function isPlainObject(value) {
2
+ return value && typeof value === 'object' && !Array.isArray(value);
3
+ }
4
+
5
+ function unwrapResultPayload(result) {
6
+ let current = result;
7
+
8
+ while (
9
+ isPlainObject(current)
10
+ && current.type === 'result'
11
+ && isPlainObject(current.data)
12
+ && current.message === undefined
13
+ && current.skill === undefined
14
+ ) {
15
+ current = current.data;
16
+ }
17
+
18
+ return current;
19
+ }
20
+
21
+ function pruneDisplayValue(value) {
22
+ if (value === null || value === undefined) {
23
+ return undefined;
24
+ }
25
+
26
+ if (Array.isArray(value)) {
27
+ const items = value
28
+ .map(pruneDisplayValue)
29
+ .filter((item) => item !== undefined);
30
+ return items.length > 0 ? items : undefined;
31
+ }
32
+
33
+ if (!isPlainObject(value)) {
34
+ return value;
35
+ }
36
+
37
+ const output = {};
38
+ for (const [key, child] of Object.entries(value)) {
39
+ if ([
40
+ 'type',
41
+ 'skill',
42
+ 'message',
43
+ 'timestamp',
44
+ 'input',
45
+ 'success',
46
+ 'metadata'
47
+ ].includes(key)) {
48
+ continue;
49
+ }
50
+
51
+ const pruned = pruneDisplayValue(child);
52
+ if (pruned !== undefined) {
53
+ output[key] = pruned;
54
+ }
55
+ }
56
+
57
+ return Object.keys(output).length > 0 ? output : undefined;
58
+ }
59
+
60
+ function formatScalar(value) {
61
+ if (typeof value === 'string') return value;
62
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
63
+ return JSON.stringify(value);
64
+ }
65
+
66
+ function renderValueLines(value, indent = '') {
67
+ if (value === undefined) return [];
68
+
69
+ if (Array.isArray(value)) {
70
+ if (value.length === 0) return [];
71
+
72
+ const allScalars = value.every((item) => !isPlainObject(item) && !Array.isArray(item));
73
+ if (allScalars) {
74
+ return [indent + value.map(formatScalar).join(', ')];
75
+ }
76
+
77
+ const lines = [];
78
+ for (const item of value) {
79
+ if (isPlainObject(item) || Array.isArray(item)) {
80
+ const nested = renderValueLines(item, indent + ' ');
81
+ if (nested.length > 0) {
82
+ lines.push(indent + '-');
83
+ lines.push(...nested);
84
+ }
85
+ } else {
86
+ lines.push(indent + '- ' + formatScalar(item));
87
+ }
88
+ }
89
+ return lines;
90
+ }
91
+
92
+ if (!isPlainObject(value)) {
93
+ return [indent + formatScalar(value)];
94
+ }
95
+
96
+ const lines = [];
97
+ for (const [key, child] of Object.entries(value)) {
98
+ if (Array.isArray(child)) {
99
+ const rendered = renderValueLines(child, indent + ' ');
100
+ if (rendered.length === 1 && !rendered[0].startsWith(indent + ' -')) {
101
+ lines.push(`${indent}${key}: ${rendered[0].trimStart()}`);
102
+ } else if (rendered.length > 0) {
103
+ lines.push(`${indent}${key}:`);
104
+ lines.push(...rendered);
105
+ }
106
+ continue;
107
+ }
108
+
109
+ if (isPlainObject(child)) {
110
+ lines.push(`${indent}${key}:`);
111
+ lines.push(...renderValueLines(child, indent + ' '));
112
+ continue;
113
+ }
114
+
115
+ lines.push(`${indent}${key}: ${formatScalar(child)}`);
116
+ }
117
+
118
+ return lines;
119
+ }
120
+
121
+ function formatResultText(result) {
122
+ if (result?.type === 'learning') {
123
+ return result.message || 'Learning recorded';
124
+ }
125
+
126
+ if (result?.type === 'error') {
127
+ return 'Error: ' + result.message;
128
+ }
129
+
130
+ const payload = unwrapResultPayload(result);
131
+
132
+ if (typeof payload === 'string') {
133
+ return payload;
134
+ }
135
+
136
+ if (!isPlainObject(payload)) {
137
+ return payload === undefined ? '' : formatScalar(payload);
138
+ }
139
+
140
+ const message = payload.message;
141
+ const details = pruneDisplayValue(payload.data);
142
+ const detailLines = renderValueLines(details);
143
+
144
+ if (message && detailLines.length > 0) {
145
+ return [message, '', ...detailLines].join('\n');
146
+ }
147
+
148
+ if (message) {
149
+ return message;
150
+ }
151
+
152
+ if (detailLines.length > 0) {
153
+ return detailLines.join('\n');
154
+ }
155
+
156
+ return JSON.stringify(payload, null, 2);
157
+ }
158
+
159
+ module.exports = {
160
+ formatResultText,
161
+ pruneDisplayValue,
162
+ renderValueLines,
163
+ unwrapResultPayload
164
+ };
package/tui/ui.js ADDED
@@ -0,0 +1,186 @@
1
+ const LEVEL_NAMES = ['Egg', 'Hatchling', 'Juvenile', 'Adult', 'Elder', 'Ascended'];
2
+ const LEVEL_STATES = ['dormant', 'awakening', 'growing', 'soaring', 'wise', 'transcendent'];
3
+ const LEVEL_COLORS = ['gray', 'cyan', 'cyan', 'cyanBright', 'cyanBright', 'cyanBright'];
4
+
5
+ const UNICODE_DRAGON_ARTS = {
6
+ 0: [
7
+ ' ╭─────╮ ',
8
+ ' ╱ ╭─╮ ╲ ',
9
+ ' │ │ │ │ ',
10
+ ' │ │ ◎ │ │ ',
11
+ ' │ ╰─╯ │ ',
12
+ ' ╲ ╱ ',
13
+ ' ╰─────╯ ',
14
+ ],
15
+ 1: [
16
+ ' ╭──╮ ',
17
+ ' ╭────╯ ╰───╮ ',
18
+ ' ╱ ◎ ╰─╯ ╲ ',
19
+ ' ╱ ▽ ╲ ',
20
+ ' ╲ ╱╲ ╱╲ ╱ ',
21
+ ' ╲╱╱ ╲╱╱ ╲╱╲╱ ',
22
+ ],
23
+ 2: [
24
+ ' ╭─╮ ╭─╮ ',
25
+ ' ╭────╯ ╰──╯ ╰───╮ ',
26
+ ' ╱ ◎ ╰──╯ ╲ ',
27
+ ' ╱ ╭────────╮ ╲ ',
28
+ ' ╲ ╱ ╱╱╱╱╱╱╱╱ ╲ ╱ ',
29
+ ' ╲───╯ ╱╱╱╱╱╱╱╱╱╱ ╰──╱ ',
30
+ ' ╰─╯ ╰─╯ ',
31
+ ],
32
+ 3: [
33
+ ' ╭───╮ ╭───╮ ',
34
+ ' ╭───╯ ╰──╯ ╰───╮ ',
35
+ ' ╱ ◎ ╰───╯ ╲ ',
36
+ '│ ╭──────────╮ │ ',
37
+ '│ ╱ ╱╱╱╱╱╱╱╱╱╱ ╲ │ ',
38
+ ' ╲──╯ ╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',
39
+ ' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╱ ',
40
+ ' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',
41
+ ' ╰───╯ ╰───╯ ',
42
+ ],
43
+ 4: [
44
+ ' ╭───╮ ╭───╮ ',
45
+ '╭───╯ ╰──────╯ ╰───╮ ',
46
+ '│ ◎ ╰───╯ │ ',
47
+ '│ ╭────────────╮ │ ',
48
+ '│ ╱ ╱╱╱╱╱╱╱╱╱╱╱╱ ╲ │ ',
49
+ ' ╲───╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰──╯ ',
50
+ ' ╲ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╲ ',
51
+ ' ╲─╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰─╲ ',
52
+ ' ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ',
53
+ ' ╰───╯ ╰───╯ ',
54
+ ],
55
+ 5: [
56
+ ' ★ ╭───╮ ╭───╮ ★ ',
57
+ '╭─╯ ╰──╯ ╰──╯ ╰─╮ ',
58
+ '│ ◎ ╰───╯ │ ',
59
+ '│ ╭──────────────╮ │ ',
60
+ '│ ╱ ★╱╱╱╱╱╱╱╱╱╱★╱╱ ╲ │ ',
61
+ ' ╲────╯ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ╰───╯ ',
62
+ ' ╲ ╱╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱ ╲ ',
63
+ ' ╲──╯╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╰──╲ ',
64
+ ' ╲─╯╱╱╱★╱╱╱╱╱╱╱╱★╱╱╱╱╱╰──╲ ',
65
+ ' ★ ╲╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱╱ ★ ',
66
+ ' ╰───╯ ╰───╯ ',
67
+ ],
68
+ };
69
+
70
+ const ASCII_DRAGON_ARTS = {
71
+ 0: [
72
+ ' /-----\\ ',
73
+ ' / /-\\ \\ ',
74
+ ' | | o | | ',
75
+ ' | | | | ',
76
+ ' | \\_/ | ',
77
+ ' \\ / ',
78
+ ' \\-----/ ',
79
+ ],
80
+ 1: [
81
+ ' /--\\ ',
82
+ ' /----/ \\---\\ ',
83
+ ' / o \\/ \\ ',
84
+ ' \\ /\\ /\\ / ',
85
+ ' \\/\\/ \\/\\/ \\/ ',
86
+ ],
87
+ 2: [
88
+ ' /-\\ /-\\ ',
89
+ ' /----/ \\/ \\---\\ ',
90
+ ' / o __ \\ ',
91
+ ' \\ /--------\\ / ',
92
+ ' \\---/ ////// \\/ ',
93
+ ' \\_/ \\_/ ',
94
+ ],
95
+ 3: [
96
+ ' /---\\ /---\\ ',
97
+ ' /---/ \\/ \\---\\ ',
98
+ '/ o __ \\ ',
99
+ '| /----------\\ | ',
100
+ ' \\--/ //////// \\-/ ',
101
+ ' \\/ ////////// \\/ ',
102
+ ' \\_/ \\_/ ',
103
+ ],
104
+ 4: [
105
+ ' /---\\ /---\\ ',
106
+ '/---/ \\____/ \\---\\ ',
107
+ '| o __ | ',
108
+ '| /------------\\ | ',
109
+ ' \\-/ //////////// \\-/ ',
110
+ ' / //////////// \\ ',
111
+ ' /_/ \\_\\ ',
112
+ ],
113
+ 5: [
114
+ ' * /---\\ /---\\ * ',
115
+ '/-/ \\______/ \\-\\ ',
116
+ '| o __ | ',
117
+ '| /--------------\\ | ',
118
+ ' \\-/ /////**///// \\-/ ',
119
+ ' / ///// ///// \\ ',
120
+ ' /_/ ** ** \\_\\ ',
121
+ ' * \\______________/ * ',
122
+ ],
123
+ };
124
+
125
+ function getBorderStyle(asciiMode) {
126
+ return asciiMode ? 'classic' : 'single';
127
+ }
128
+
129
+ function getProgressBar(width, progress, asciiMode) {
130
+ const safeWidth = Math.max(1, width);
131
+ const filled = Math.round(Math.max(0, Math.min(1, progress)) * safeWidth);
132
+ const fullChar = asciiMode ? '#' : '\u2588';
133
+ const emptyChar = asciiMode ? '-' : '\u2591';
134
+ return fullChar.repeat(filled) + emptyChar.repeat(safeWidth - filled);
135
+ }
136
+
137
+ function getStatusDot(active, asciiMode) {
138
+ if (asciiMode) {
139
+ return active ? ' *' : ' o';
140
+ }
141
+ return active ? ' \u25CF' : ' \u25CB';
142
+ }
143
+
144
+ function getSelectionPrefix(selected, asciiMode) {
145
+ if (!selected) return ' ';
146
+ return asciiMode ? '> ' : '\u25B6 ';
147
+ }
148
+
149
+ function getCheckMark(success, asciiMode) {
150
+ if (asciiMode) {
151
+ return success ? 'ok' : 'x';
152
+ }
153
+ return success ? '\u2713' : '\u2717';
154
+ }
155
+
156
+ function getDragonArt(level, { asciiMode = false, compact = false } = {}) {
157
+ const source = asciiMode ? ASCII_DRAGON_ARTS : UNICODE_DRAGON_ARTS;
158
+ const art = source[level] || source[0];
159
+ return compact ? art.slice(0, Math.min(4, art.length)) : art;
160
+ }
161
+
162
+ function isExitCommand(input) {
163
+ const normalized = String(input || '').trim().toLowerCase();
164
+ return [
165
+ 'exit',
166
+ 'quit',
167
+ '/exit',
168
+ '/quit',
169
+ ':q',
170
+ ':quit',
171
+ ':exit'
172
+ ].includes(normalized);
173
+ }
174
+
175
+ module.exports = {
176
+ LEVEL_COLORS,
177
+ LEVEL_NAMES,
178
+ LEVEL_STATES,
179
+ getBorderStyle,
180
+ getCheckMark,
181
+ getDragonArt,
182
+ getProgressBar,
183
+ getSelectionPrefix,
184
+ getStatusDot,
185
+ isExitCommand
186
+ };