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.
- package/README.md +41 -0
- package/assets/jerom_16x16.png +0 -0
- package/dist/arbiter.d.ts +43 -0
- package/dist/arbiter.js +486 -0
- package/dist/context-analyzer.d.ts +15 -0
- package/dist/context-analyzer.js +603 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +165 -0
- package/dist/orchestrator.d.ts +31 -0
- package/dist/orchestrator.js +227 -0
- package/dist/router.d.ts +187 -0
- package/dist/router.js +1135 -0
- package/dist/router.test.d.ts +15 -0
- package/dist/router.test.js +95 -0
- package/dist/session-persistence.d.ts +9 -0
- package/dist/session-persistence.js +63 -0
- package/dist/session-persistence.test.d.ts +1 -0
- package/dist/session-persistence.test.js +165 -0
- package/dist/sound.d.ts +31 -0
- package/dist/sound.js +50 -0
- package/dist/state.d.ts +72 -0
- package/dist/state.js +107 -0
- package/dist/state.test.d.ts +1 -0
- package/dist/state.test.js +194 -0
- package/dist/test-headless.d.ts +1 -0
- package/dist/test-headless.js +155 -0
- package/dist/tui/index.d.ts +14 -0
- package/dist/tui/index.js +17 -0
- package/dist/tui/layout.d.ts +30 -0
- package/dist/tui/layout.js +200 -0
- package/dist/tui/render.d.ts +57 -0
- package/dist/tui/render.js +266 -0
- package/dist/tui/scene.d.ts +64 -0
- package/dist/tui/scene.js +366 -0
- package/dist/tui/screens/CharacterSelect-termkit.d.ts +18 -0
- package/dist/tui/screens/CharacterSelect-termkit.js +216 -0
- package/dist/tui/screens/ForestIntro-termkit.d.ts +15 -0
- package/dist/tui/screens/ForestIntro-termkit.js +856 -0
- package/dist/tui/screens/GitignoreCheck-termkit.d.ts +14 -0
- package/dist/tui/screens/GitignoreCheck-termkit.js +185 -0
- package/dist/tui/screens/TitleScreen-termkit.d.ts +14 -0
- package/dist/tui/screens/TitleScreen-termkit.js +132 -0
- package/dist/tui/screens/index.d.ts +9 -0
- package/dist/tui/screens/index.js +10 -0
- package/dist/tui/tileset.d.ts +97 -0
- package/dist/tui/tileset.js +237 -0
- package/dist/tui/tui-termkit.d.ts +34 -0
- package/dist/tui/tui-termkit.js +2602 -0
- package/dist/tui/types.d.ts +41 -0
- package/dist/tui/types.js +4 -0
- 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;
|