arbiter-ai 1.0.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.
Files changed (51) hide show
  1. package/README.md +41 -0
  2. package/assets/jerom_16x16.png +0 -0
  3. package/dist/arbiter.d.ts +43 -0
  4. package/dist/arbiter.js +486 -0
  5. package/dist/context-analyzer.d.ts +15 -0
  6. package/dist/context-analyzer.js +603 -0
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +165 -0
  9. package/dist/orchestrator.d.ts +31 -0
  10. package/dist/orchestrator.js +227 -0
  11. package/dist/router.d.ts +187 -0
  12. package/dist/router.js +1135 -0
  13. package/dist/router.test.d.ts +15 -0
  14. package/dist/router.test.js +95 -0
  15. package/dist/session-persistence.d.ts +9 -0
  16. package/dist/session-persistence.js +63 -0
  17. package/dist/session-persistence.test.d.ts +1 -0
  18. package/dist/session-persistence.test.js +165 -0
  19. package/dist/sound.d.ts +31 -0
  20. package/dist/sound.js +50 -0
  21. package/dist/state.d.ts +72 -0
  22. package/dist/state.js +107 -0
  23. package/dist/state.test.d.ts +1 -0
  24. package/dist/state.test.js +194 -0
  25. package/dist/test-headless.d.ts +1 -0
  26. package/dist/test-headless.js +155 -0
  27. package/dist/tui/index.d.ts +14 -0
  28. package/dist/tui/index.js +17 -0
  29. package/dist/tui/layout.d.ts +30 -0
  30. package/dist/tui/layout.js +200 -0
  31. package/dist/tui/render.d.ts +57 -0
  32. package/dist/tui/render.js +266 -0
  33. package/dist/tui/scene.d.ts +64 -0
  34. package/dist/tui/scene.js +366 -0
  35. package/dist/tui/screens/CharacterSelect-termkit.d.ts +18 -0
  36. package/dist/tui/screens/CharacterSelect-termkit.js +216 -0
  37. package/dist/tui/screens/ForestIntro-termkit.d.ts +15 -0
  38. package/dist/tui/screens/ForestIntro-termkit.js +856 -0
  39. package/dist/tui/screens/GitignoreCheck-termkit.d.ts +14 -0
  40. package/dist/tui/screens/GitignoreCheck-termkit.js +185 -0
  41. package/dist/tui/screens/TitleScreen-termkit.d.ts +14 -0
  42. package/dist/tui/screens/TitleScreen-termkit.js +132 -0
  43. package/dist/tui/screens/index.d.ts +9 -0
  44. package/dist/tui/screens/index.js +10 -0
  45. package/dist/tui/tileset.d.ts +97 -0
  46. package/dist/tui/tileset.js +237 -0
  47. package/dist/tui/tui-termkit.d.ts +34 -0
  48. package/dist/tui/tui-termkit.js +2602 -0
  49. package/dist/tui/types.d.ts +41 -0
  50. package/dist/tui/types.js +4 -0
  51. package/package.json +71 -0
@@ -0,0 +1,194 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+ import { addMessage, clearCurrentOrchestrator, createInitialState, setCurrentOrchestrator, setMode, toRoman, updateArbiterContext, updateOrchestratorContext, updateOrchestratorTool, } from './state.js';
3
+ describe('state', () => {
4
+ describe('createInitialState', () => {
5
+ it('should create state with default values', () => {
6
+ const state = createInitialState();
7
+ expect(state.mode).toBe('human_to_arbiter');
8
+ expect(state.arbiterSessionId).toBeNull();
9
+ expect(state.arbiterContextPercent).toBe(0);
10
+ expect(state.currentOrchestrator).toBeNull();
11
+ expect(state.conversationLog).toEqual([]);
12
+ expect(state.crashCount).toBe(0);
13
+ expect(state.requirementsPath).toBeNull();
14
+ });
15
+ it('should create independent state objects', () => {
16
+ const state1 = createInitialState();
17
+ const state2 = createInitialState();
18
+ state1.crashCount = 5;
19
+ expect(state2.crashCount).toBe(0);
20
+ });
21
+ });
22
+ describe('updateArbiterContext', () => {
23
+ let state;
24
+ beforeEach(() => {
25
+ state = createInitialState();
26
+ });
27
+ it('should update arbiter context percentage', () => {
28
+ updateArbiterContext(state, 50);
29
+ expect(state.arbiterContextPercent).toBe(50);
30
+ });
31
+ it('should allow 0 percent', () => {
32
+ updateArbiterContext(state, 0);
33
+ expect(state.arbiterContextPercent).toBe(0);
34
+ });
35
+ it('should allow 100 percent', () => {
36
+ updateArbiterContext(state, 100);
37
+ expect(state.arbiterContextPercent).toBe(100);
38
+ });
39
+ });
40
+ describe('updateOrchestratorContext', () => {
41
+ let state;
42
+ beforeEach(() => {
43
+ state = createInitialState();
44
+ });
45
+ it('should do nothing when no orchestrator is set', () => {
46
+ updateOrchestratorContext(state, 50);
47
+ expect(state.currentOrchestrator).toBeNull();
48
+ });
49
+ it('should update orchestrator context when one exists', () => {
50
+ setCurrentOrchestrator(state, { id: 'test-id', sessionId: 'session-1', number: 1 });
51
+ updateOrchestratorContext(state, 75);
52
+ expect(state.currentOrchestrator?.contextPercent).toBe(75);
53
+ });
54
+ });
55
+ describe('setCurrentOrchestrator', () => {
56
+ let state;
57
+ beforeEach(() => {
58
+ state = createInitialState();
59
+ });
60
+ it('should set orchestrator with provided values', () => {
61
+ setCurrentOrchestrator(state, { id: 'orch-123', sessionId: 'sess-456', number: 2 });
62
+ expect(state.currentOrchestrator).toEqual({
63
+ id: 'orch-123',
64
+ sessionId: 'sess-456',
65
+ number: 2,
66
+ contextPercent: 0,
67
+ currentTool: null,
68
+ toolCallCount: 0,
69
+ });
70
+ });
71
+ it('should replace existing orchestrator', () => {
72
+ setCurrentOrchestrator(state, { id: 'first', sessionId: 'sess-1', number: 1 });
73
+ setCurrentOrchestrator(state, { id: 'second', sessionId: 'sess-2', number: 2 });
74
+ expect(state.currentOrchestrator?.id).toBe('second');
75
+ expect(state.currentOrchestrator?.number).toBe(2);
76
+ });
77
+ });
78
+ describe('clearCurrentOrchestrator', () => {
79
+ let state;
80
+ beforeEach(() => {
81
+ state = createInitialState();
82
+ });
83
+ it('should clear orchestrator when one exists', () => {
84
+ setCurrentOrchestrator(state, { id: 'test', sessionId: 'sess', number: 1 });
85
+ clearCurrentOrchestrator(state);
86
+ expect(state.currentOrchestrator).toBeNull();
87
+ });
88
+ it('should be safe to call when no orchestrator exists', () => {
89
+ clearCurrentOrchestrator(state);
90
+ expect(state.currentOrchestrator).toBeNull();
91
+ });
92
+ });
93
+ describe('setMode', () => {
94
+ let state;
95
+ beforeEach(() => {
96
+ state = createInitialState();
97
+ });
98
+ it('should set mode to arbiter_to_orchestrator', () => {
99
+ setMode(state, 'arbiter_to_orchestrator');
100
+ expect(state.mode).toBe('arbiter_to_orchestrator');
101
+ });
102
+ it('should set mode back to human_to_arbiter', () => {
103
+ setMode(state, 'arbiter_to_orchestrator');
104
+ setMode(state, 'human_to_arbiter');
105
+ expect(state.mode).toBe('human_to_arbiter');
106
+ });
107
+ });
108
+ describe('addMessage', () => {
109
+ let state;
110
+ beforeEach(() => {
111
+ state = createInitialState();
112
+ });
113
+ it('should add message to empty conversation log', () => {
114
+ addMessage(state, 'human', 'Hello');
115
+ expect(state.conversationLog).toHaveLength(1);
116
+ expect(state.conversationLog[0].speaker).toBe('human');
117
+ expect(state.conversationLog[0].text).toBe('Hello');
118
+ expect(state.conversationLog[0].timestamp).toBeInstanceOf(Date);
119
+ });
120
+ it('should add multiple messages in order', () => {
121
+ addMessage(state, 'human', 'First');
122
+ addMessage(state, 'arbiter', 'Second');
123
+ addMessage(state, 'Orchestrator I', 'Third');
124
+ expect(state.conversationLog).toHaveLength(3);
125
+ expect(state.conversationLog[0].speaker).toBe('human');
126
+ expect(state.conversationLog[1].speaker).toBe('arbiter');
127
+ expect(state.conversationLog[2].speaker).toBe('Orchestrator I');
128
+ });
129
+ it('should accept any string as speaker', () => {
130
+ addMessage(state, 'Orchestrator II', 'Test');
131
+ expect(state.conversationLog[0].speaker).toBe('Orchestrator II');
132
+ });
133
+ });
134
+ describe('updateOrchestratorTool', () => {
135
+ let state;
136
+ beforeEach(() => {
137
+ state = createInitialState();
138
+ });
139
+ it('should do nothing when no orchestrator is set', () => {
140
+ updateOrchestratorTool(state, 'Edit', 5);
141
+ expect(state.currentOrchestrator).toBeNull();
142
+ });
143
+ it('should update tool info when orchestrator exists', () => {
144
+ setCurrentOrchestrator(state, { id: 'test', sessionId: 'sess', number: 1 });
145
+ updateOrchestratorTool(state, 'Read', 10);
146
+ expect(state.currentOrchestrator?.currentTool).toBe('Read');
147
+ expect(state.currentOrchestrator?.toolCallCount).toBe(10);
148
+ });
149
+ it('should allow null tool', () => {
150
+ setCurrentOrchestrator(state, { id: 'test', sessionId: 'sess', number: 1 });
151
+ updateOrchestratorTool(state, 'Edit', 5);
152
+ updateOrchestratorTool(state, null, 0);
153
+ expect(state.currentOrchestrator?.currentTool).toBeNull();
154
+ expect(state.currentOrchestrator?.toolCallCount).toBe(0);
155
+ });
156
+ });
157
+ describe('toRoman', () => {
158
+ it('should convert basic numbers', () => {
159
+ expect(toRoman(1)).toBe('I');
160
+ expect(toRoman(5)).toBe('V');
161
+ expect(toRoman(10)).toBe('X');
162
+ expect(toRoman(50)).toBe('L');
163
+ expect(toRoman(100)).toBe('C');
164
+ expect(toRoman(500)).toBe('D');
165
+ expect(toRoman(1000)).toBe('M');
166
+ });
167
+ it('should convert subtractive notation numbers', () => {
168
+ expect(toRoman(4)).toBe('IV');
169
+ expect(toRoman(9)).toBe('IX');
170
+ expect(toRoman(40)).toBe('XL');
171
+ expect(toRoman(90)).toBe('XC');
172
+ expect(toRoman(400)).toBe('CD');
173
+ expect(toRoman(900)).toBe('CM');
174
+ });
175
+ it('should convert complex numbers', () => {
176
+ expect(toRoman(2)).toBe('II');
177
+ expect(toRoman(3)).toBe('III');
178
+ expect(toRoman(14)).toBe('XIV');
179
+ expect(toRoman(39)).toBe('XXXIX');
180
+ expect(toRoman(246)).toBe('CCXLVI');
181
+ expect(toRoman(789)).toBe('DCCLXXXIX');
182
+ expect(toRoman(2421)).toBe('MMCDXXI');
183
+ expect(toRoman(3999)).toBe('MMMCMXCIX');
184
+ });
185
+ it('should throw for numbers less than 1', () => {
186
+ expect(() => toRoman(0)).toThrow('Number must be between 1 and 3999');
187
+ expect(() => toRoman(-1)).toThrow('Number must be between 1 and 3999');
188
+ });
189
+ it('should throw for numbers greater than 3999', () => {
190
+ expect(() => toRoman(4000)).toThrow('Number must be between 1 and 3999');
191
+ expect(() => toRoman(10000)).toThrow('Number must be between 1 and 3999');
192
+ });
193
+ });
194
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,155 @@
1
+ // Headless test script for Arbiter core functionality
2
+ // Tests the Router and State without TUI
3
+ import { Router } from './router.js';
4
+ import { createInitialState } from './state.js';
5
+ // Test timeout in milliseconds
6
+ const TEST_TIMEOUT_MS = 120000;
7
+ // Track if we've received responses
8
+ let arbiterResponseCount = 0;
9
+ let orchestratorResponseCount = 0;
10
+ let testComplete = false;
11
+ /**
12
+ * Create mock callbacks that log to console
13
+ */
14
+ function createMockCallbacks() {
15
+ return {
16
+ onHumanMessage: (text) => {
17
+ console.log('\n=== HUMAN MESSAGE ===');
18
+ console.log(text);
19
+ console.log('======================\n');
20
+ },
21
+ onArbiterMessage: (text) => {
22
+ console.log('\n=== ARBITER MESSAGE ===');
23
+ console.log(text);
24
+ console.log('========================\n');
25
+ arbiterResponseCount++;
26
+ },
27
+ onOrchestratorMessage: (orchestratorNumber, text) => {
28
+ console.log(`\n=== ORCHESTRATOR ${orchestratorNumber} MESSAGE ===`);
29
+ console.log(text);
30
+ console.log('================================\n');
31
+ orchestratorResponseCount++;
32
+ },
33
+ onContextUpdate: (arbiterPercent, orchestratorPercent) => {
34
+ console.log(`[Context] Arbiter: ${arbiterPercent.toFixed(1)}%, Orchestrator: ${orchestratorPercent !== null ? `${orchestratorPercent.toFixed(1)}%` : 'N/A'}`);
35
+ },
36
+ onToolUse: (tool, count) => {
37
+ console.log(`[Tool Use] ${tool} (total calls: ${count})`);
38
+ },
39
+ onModeChange: (mode) => {
40
+ console.log(`[Mode Changed] ${mode}`);
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * Sleep for a specified number of milliseconds
46
+ */
47
+ function sleep(ms) {
48
+ return new Promise((resolve) => setTimeout(resolve, ms));
49
+ }
50
+ /**
51
+ * Main test function
52
+ */
53
+ async function runTest() {
54
+ console.log('===========================================');
55
+ console.log(' ARBITER HEADLESS TEST');
56
+ console.log('===========================================\n');
57
+ // Create initial state
58
+ console.log('[Setup] Creating initial state...');
59
+ const state = createInitialState();
60
+ console.log('[Setup] Initial state created:', JSON.stringify(state, null, 2));
61
+ // Create mock callbacks
62
+ console.log('[Setup] Creating mock callbacks...');
63
+ const callbacks = createMockCallbacks();
64
+ // Create router
65
+ console.log('[Setup] Creating router...');
66
+ const router = new Router(state, callbacks);
67
+ // Set up timeout
68
+ const timeoutId = setTimeout(async () => {
69
+ console.log('\n[Timeout] Test timeout reached (120 seconds)');
70
+ console.log('[Cleanup] Stopping router...');
71
+ testComplete = true;
72
+ await router.stop();
73
+ printSummary();
74
+ process.exit(0);
75
+ }, TEST_TIMEOUT_MS);
76
+ try {
77
+ // Start the router (initializes Arbiter session)
78
+ console.log('\n[Test] Starting router (initializing Arbiter session)...');
79
+ await router.start();
80
+ console.log('\n[Test] Router started.');
81
+ // Send first test message
82
+ console.log("\n[Test] Sending first message: 'Hello, what are you?'");
83
+ await router.sendHumanMessage('Hello, what are you?');
84
+ // Wait for first response before sending second message
85
+ console.log('[Test] Waiting for Arbiter response...');
86
+ const startTime = Date.now();
87
+ while (arbiterResponseCount === 0 && Date.now() - startTime < 60000) {
88
+ await sleep(500);
89
+ }
90
+ if (arbiterResponseCount === 0) {
91
+ console.log('[Warning] No Arbiter response after 60 seconds');
92
+ }
93
+ else {
94
+ console.log('[Test] First response received!');
95
+ }
96
+ // Send second test message immediately after receiving first response
97
+ console.log("\n[Test] Sending second message: 'Please spawn an orchestrator to list the files in the current directory'");
98
+ await router.sendHumanMessage('Please spawn an orchestrator to list the files in the current directory');
99
+ console.log('[Test] Waiting for Orchestrator to be spawned and do work...');
100
+ // Wait and check periodically
101
+ let waitTime = 0;
102
+ const checkInterval = 2000;
103
+ while (waitTime < TEST_TIMEOUT_MS - 15000 && !testComplete) {
104
+ await sleep(checkInterval);
105
+ waitTime += checkInterval;
106
+ console.log(`[Status] Elapsed: ${waitTime / 1000}s, Arbiter responses: ${arbiterResponseCount}, Orchestrator responses: ${orchestratorResponseCount}`);
107
+ // If orchestrator has responded, we can consider the test successful
108
+ if (orchestratorResponseCount > 0) {
109
+ console.log('\n[Success] Orchestrator has responded!');
110
+ await sleep(3000); // Give a bit more time for any final responses
111
+ break;
112
+ }
113
+ }
114
+ // Clean up
115
+ clearTimeout(timeoutId);
116
+ console.log('\n[Cleanup] Stopping router...');
117
+ await router.stop();
118
+ printSummary();
119
+ }
120
+ catch (error) {
121
+ clearTimeout(timeoutId);
122
+ console.error('\n[Error] Test failed with error:');
123
+ console.error(error);
124
+ try {
125
+ await router.stop();
126
+ }
127
+ catch (stopError) {
128
+ console.error('[Error] Failed to stop router:', stopError);
129
+ }
130
+ process.exit(1);
131
+ }
132
+ }
133
+ /**
134
+ * Print test summary
135
+ */
136
+ function printSummary() {
137
+ console.log('\n===========================================');
138
+ console.log(' TEST SUMMARY');
139
+ console.log('===========================================');
140
+ console.log(`Arbiter responses received: ${arbiterResponseCount}`);
141
+ console.log(`Orchestrator responses received: ${orchestratorResponseCount}`);
142
+ console.log(`Test result: ${arbiterResponseCount > 0 ? 'PASSED (received Arbiter response)' : 'NEEDS REVIEW'}`);
143
+ console.log('===========================================\n');
144
+ }
145
+ // Run the test
146
+ console.log('[Init] Starting headless test...\n');
147
+ runTest()
148
+ .then(() => {
149
+ console.log('[Done] Test completed.');
150
+ process.exit(0);
151
+ })
152
+ .catch((error) => {
153
+ console.error('[Fatal] Unhandled error:', error);
154
+ process.exit(1);
155
+ });
@@ -0,0 +1,14 @@
1
+ /**
2
+ * TUI module entry point
3
+ * RPG-style terminal interface with wizard council theme
4
+ *
5
+ * This module uses a terminal-kit based implementation with Strategy 5
6
+ * (minimal redraws) for flicker-free animation and input handling.
7
+ */
8
+ export type { DebugLogEntry, RouterCallbacks } from '../router.js';
9
+ export { type CharacterSelectResult, showCharacterSelect, } from './screens/CharacterSelect-termkit.js';
10
+ export { showForestIntro } from './screens/ForestIntro-termkit.js';
11
+ export { checkGitignore } from './screens/GitignoreCheck-termkit.js';
12
+ export { showTitleScreen } from './screens/TitleScreen-termkit.js';
13
+ export { createTUI, type TUI } from './tui-termkit.js';
14
+ export type { WaitingState } from './types.js';
@@ -0,0 +1,17 @@
1
+ /**
2
+ * TUI module entry point
3
+ * RPG-style terminal interface with wizard council theme
4
+ *
5
+ * This module uses a terminal-kit based implementation with Strategy 5
6
+ * (minimal redraws) for flicker-free animation and input handling.
7
+ */
8
+ // Re-export the terminal-kit based CharacterSelect
9
+ export { showCharacterSelect, } from './screens/CharacterSelect-termkit.js';
10
+ // Re-export the terminal-kit based ForestIntro
11
+ export { showForestIntro } from './screens/ForestIntro-termkit.js';
12
+ // Re-export the terminal-kit based GitignoreCheck
13
+ export { checkGitignore } from './screens/GitignoreCheck-termkit.js';
14
+ // Re-export the terminal-kit based TitleScreen
15
+ export { showTitleScreen } from './screens/TitleScreen-termkit.js';
16
+ // Re-export the terminal-kit based TUI
17
+ export { createTUI } from './tui-termkit.js';
@@ -0,0 +1,30 @@
1
+ import blessed from 'blessed';
2
+ /**
3
+ * Layout elements interface - exposes all UI components
4
+ */
5
+ export interface LayoutElements {
6
+ screen: blessed.Widgets.Screen;
7
+ titleBox: blessed.Widgets.BoxElement;
8
+ conversationBox: blessed.Widgets.BoxElement;
9
+ statusBox: blessed.Widgets.BoxElement;
10
+ inputBox: blessed.Widgets.TextboxElement;
11
+ }
12
+ /**
13
+ * Creates the blessed screen layout matching the architecture doc design
14
+ *
15
+ * Layout:
16
+ * - Full terminal takeover
17
+ * - Title area at top with "THE ARBITER" and subtitle
18
+ * - Main conversation area (scrollable)
19
+ * - Status bar showing Arbiter context %, Orchestrator context %, current tool
20
+ * - Input box at bottom with ">" prompt
21
+ */
22
+ export declare function createLayout(): LayoutElements;
23
+ /**
24
+ * Creates the input prompt line with box drawing characters
25
+ */
26
+ export declare function createInputPrompt(width: number): string;
27
+ /**
28
+ * Creates a horizontal separator line
29
+ */
30
+ export declare function createSeparator(width: number): string;
@@ -0,0 +1,200 @@
1
+ // TUI layout configuration using blessed
2
+ // Defines the screen layout, panels, and UI structure
3
+ import blessed from 'blessed';
4
+ /**
5
+ * Box drawing characters for roguelike aesthetic
6
+ */
7
+ const BOX_CHARS = {
8
+ topLeft: '\u2554', // ╔
9
+ topRight: '\u2557', // ╗
10
+ bottomLeft: '\u255A', // ╚
11
+ bottomRight: '\u255D', // ╝
12
+ horizontal: '\u2550', // ═
13
+ vertical: '\u2551', // ║
14
+ leftT: '\u2560', // ╠
15
+ rightT: '\u2563', // ╣
16
+ };
17
+ /**
18
+ * Creates the blessed screen layout matching the architecture doc design
19
+ *
20
+ * Layout:
21
+ * - Full terminal takeover
22
+ * - Title area at top with "THE ARBITER" and subtitle
23
+ * - Main conversation area (scrollable)
24
+ * - Status bar showing Arbiter context %, Orchestrator context %, current tool
25
+ * - Input box at bottom with ">" prompt
26
+ */
27
+ export function createLayout() {
28
+ // Create the main screen
29
+ const screen = blessed.screen({
30
+ smartCSR: true,
31
+ title: 'THE ARBITER',
32
+ fullUnicode: true,
33
+ dockBorders: true,
34
+ autoPadding: false,
35
+ });
36
+ // Title box at the top
37
+ const titleBox = blessed.box({
38
+ parent: screen,
39
+ top: 0,
40
+ left: 0,
41
+ width: '100%',
42
+ height: 4,
43
+ content: '',
44
+ tags: true,
45
+ style: {
46
+ fg: 'white',
47
+ bg: 'black',
48
+ },
49
+ });
50
+ // Set title content with box drawing characters
51
+ updateTitleContent(titleBox, screen.width);
52
+ // Main conversation area (scrollable)
53
+ const conversationBox = blessed.box({
54
+ parent: screen,
55
+ top: 4,
56
+ left: 0,
57
+ width: '100%',
58
+ height: '100%-10', // Leave room for status (3 lines) and input (3 lines)
59
+ content: '',
60
+ tags: true,
61
+ scrollable: true,
62
+ alwaysScroll: true,
63
+ scrollbar: {
64
+ ch: '\u2588', // █
65
+ style: {
66
+ fg: 'white',
67
+ },
68
+ },
69
+ mouse: true,
70
+ keys: true,
71
+ vi: true,
72
+ style: {
73
+ fg: 'white',
74
+ bg: 'black',
75
+ border: {
76
+ fg: 'white',
77
+ },
78
+ },
79
+ border: {
80
+ type: 'line',
81
+ },
82
+ });
83
+ // Override border characters to use double-line box drawing
84
+ conversationBox.border = {
85
+ type: 'line',
86
+ ch: ' ',
87
+ top: BOX_CHARS.horizontal,
88
+ bottom: BOX_CHARS.horizontal,
89
+ left: BOX_CHARS.vertical,
90
+ right: BOX_CHARS.vertical,
91
+ };
92
+ // Status bar area (3 lines)
93
+ const statusBox = blessed.box({
94
+ parent: screen,
95
+ bottom: 3,
96
+ left: 0,
97
+ width: '100%',
98
+ height: 4,
99
+ content: '',
100
+ tags: true,
101
+ style: {
102
+ fg: 'white',
103
+ bg: 'black',
104
+ },
105
+ });
106
+ // Input box at the bottom
107
+ const inputBox = blessed.textbox({
108
+ parent: screen,
109
+ bottom: 0,
110
+ left: 2, // Leave room for "> " prompt
111
+ width: '100%-2',
112
+ height: 3,
113
+ inputOnFocus: true,
114
+ mouse: true,
115
+ keys: true,
116
+ style: {
117
+ fg: 'white',
118
+ bg: 'black',
119
+ border: {
120
+ fg: 'white',
121
+ },
122
+ },
123
+ border: {
124
+ type: 'line',
125
+ },
126
+ });
127
+ // Create a fixed prompt label "> " that sits to the left of the input
128
+ const _promptLabel = blessed.text({
129
+ parent: screen,
130
+ bottom: 1,
131
+ left: 0,
132
+ width: 2,
133
+ height: 1,
134
+ content: '> ',
135
+ style: {
136
+ fg: 'white',
137
+ bg: 'black',
138
+ },
139
+ });
140
+ // Handle screen resize to update title width
141
+ screen.on('resize', () => {
142
+ updateTitleContent(titleBox, screen.width);
143
+ screen.render();
144
+ });
145
+ // Set up quit key bindings
146
+ screen.key(['escape', 'q', 'C-c'], () => {
147
+ return process.exit(0);
148
+ });
149
+ return {
150
+ screen,
151
+ titleBox,
152
+ conversationBox,
153
+ statusBox,
154
+ inputBox,
155
+ };
156
+ }
157
+ /**
158
+ * Updates the title box content with proper width-based formatting
159
+ */
160
+ function updateTitleContent(titleBox, width) {
161
+ const title = 'THE ARBITER';
162
+ const subtitle = 'OF THAT WHICH WAS, THAT WHICH IS, AND THAT WHICH SHALL COME TO BE';
163
+ // Calculate effective width (accounting for border characters)
164
+ const effectiveWidth = Math.max(width - 2, 80);
165
+ // Create top border
166
+ const topBorder = BOX_CHARS.topLeft + BOX_CHARS.horizontal.repeat(effectiveWidth) + BOX_CHARS.topRight;
167
+ // Center the title and subtitle
168
+ const titlePadding = Math.max(0, Math.floor((effectiveWidth - title.length) / 2));
169
+ const subtitlePadding = Math.max(0, Math.floor((effectiveWidth - subtitle.length) / 2));
170
+ const titleLine = BOX_CHARS.vertical +
171
+ ' '.repeat(titlePadding) +
172
+ `{bold}${title}{/bold}` +
173
+ ' '.repeat(effectiveWidth - titlePadding - title.length) +
174
+ BOX_CHARS.vertical;
175
+ const subtitleLine = BOX_CHARS.vertical +
176
+ ' '.repeat(subtitlePadding) +
177
+ subtitle +
178
+ ' '.repeat(Math.max(0, effectiveWidth - subtitlePadding - subtitle.length)) +
179
+ BOX_CHARS.vertical;
180
+ // Create separator
181
+ const separator = BOX_CHARS.leftT + BOX_CHARS.horizontal.repeat(effectiveWidth) + BOX_CHARS.rightT;
182
+ titleBox.setContent(`${topBorder}\n${titleLine}\n${subtitleLine}\n${separator}`);
183
+ }
184
+ /**
185
+ * Creates the input prompt line with box drawing characters
186
+ */
187
+ export function createInputPrompt(width) {
188
+ const effectiveWidth = Math.max(width - 4, 76);
189
+ const promptLine = `${BOX_CHARS.vertical} > `;
190
+ const endLine = ' '.repeat(effectiveWidth) + BOX_CHARS.vertical;
191
+ const bottomBorder = BOX_CHARS.bottomLeft + BOX_CHARS.horizontal.repeat(width - 2) + BOX_CHARS.bottomRight;
192
+ return `${promptLine + endLine}\n${bottomBorder}`;
193
+ }
194
+ /**
195
+ * Creates a horizontal separator line
196
+ */
197
+ export function createSeparator(width) {
198
+ const effectiveWidth = Math.max(width - 2, 78);
199
+ return BOX_CHARS.leftT + BOX_CHARS.horizontal.repeat(effectiveWidth) + BOX_CHARS.rightT;
200
+ }
@@ -0,0 +1,57 @@
1
+ import type { LayoutElements } from './layout.js';
2
+ import { type AppState } from '../state.js';
3
+ /**
4
+ * Generates animated dots text based on current animation frame
5
+ * Cycles through: "Working." -> "Working.." -> "Working..."
6
+ * @param baseText - The base text to animate (e.g., "Working" or "Waiting for Arbiter")
7
+ * @returns The text with animated dots appended
8
+ */
9
+ export declare function getAnimatedDots(baseText: string): string;
10
+ /**
11
+ * Advances the animation frame for the loading dots
12
+ * Should be called by an interval timer
13
+ */
14
+ export declare function advanceAnimation(): void;
15
+ /**
16
+ * Resets the animation frame to 0
17
+ */
18
+ export declare function resetAnimation(): void;
19
+ /**
20
+ * Renders the conversation log to the conversation box
21
+ * Formats messages with speakers (You:, Arbiter:, Orchestrator I:, etc.)
22
+ */
23
+ export declare function renderConversation(elements: LayoutElements, state: AppState): void;
24
+ /**
25
+ * Waiting state enum for different waiting scenarios
26
+ */
27
+ export type WaitingState = 'none' | 'arbiter' | 'orchestrator';
28
+ /**
29
+ * Renders the status bar with context percentages and current tool
30
+ *
31
+ * When orchestrator is active:
32
+ * ║ Arbiter ─────────────────────────────────────────────────── ██░░░░░░░░ 18% ║
33
+ * ║ Orchestrator I ──────────────────────────────────────────── ████████░░ 74% ║
34
+ * ║ ◈ Edit (12) ║
35
+ *
36
+ * When no orchestrator (Arbiter speaks to human):
37
+ * ║ Arbiter ─────────────────────────────────────────────────── ██░░░░░░░░ 18% ║
38
+ * ║ Awaiting your command. ║
39
+ *
40
+ * @param waitingState - Optional waiting state to show animated dots
41
+ */
42
+ export declare function renderStatus(elements: LayoutElements, state: AppState, waitingState?: WaitingState): void;
43
+ /**
44
+ * Creates an ASCII progress bar
45
+ * @param percent - Current percentage (0-100)
46
+ * @param width - Total width of the progress bar
47
+ * @returns Progress bar string like "████████░░"
48
+ */
49
+ export declare function renderProgressBar(percent: number, width: number): string;
50
+ /**
51
+ * Renders the input area with prompt
52
+ */
53
+ export declare function renderInputArea(elements: LayoutElements): void;
54
+ /**
55
+ * Updates the entire display
56
+ */
57
+ export declare function renderAll(elements: LayoutElements, state: AppState): void;