apex-dev 2.0.0 → 3.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/.local/share/amp/history.jsonl +33 -0
- package/.local/share/amp/session.json +3 -3
- package/.local/share/amp/threads/T-019c9761-858c-719b-911f-bc2e4c8cbdde.json +188 -0
- package/.local/share/amp/threads/T-019c9761-f5f3-7606-a900-ebe7f10d6e37.json +121 -0
- package/.local/share/amp/threads/T-019c9763-b1ae-729d-90aa-f59938ce912e.json +799 -0
- package/.local/share/amp/threads/T-019c9769-4a8a-77b8-beab-f48973276f9a.json +1541 -0
- package/.local/share/amp/threads/T-019c9772-edac-7075-b26e-0ada1f8697d2.json +7 -0
- package/.local/share/amp/threads/T-019c97e8-a9ab-71a1-a8f9-109c540c98bf.json +111 -0
- package/.local/share/amp/threads/T-019c97e9-2277-753c-8c5d-df745fa6cfff.json +7 -0
- package/.local/share/amp/threads/T-019c97e9-f28e-758d-9663-e37047a8ed95.json +111 -0
- package/.local/share/amp/threads/T-019c97ea-17c7-77b8-92b2-f641c069bcc9.json +71 -0
- package/.local/share/amp/threads/T-019c97ea-44c6-75b8-88bc-d88113194f6a.json +1611 -0
- package/.local/share/amp/threads/T-019c97ec-abae-7251-a5f6-693adf496a1c.json +7 -0
- package/.local/share/amp/threads/T-019c97f5-8e61-73ad-8c5d-2637abedcde6.json +1341 -0
- package/.local/share/amp/threads/T-019c989d-4f4e-7249-bde0-21d19455ccae.json +163 -0
- package/.local/share/amp/threads/T-019c989d-9024-73c4-bee8-e2ae45028a39.json +124 -0
- package/.local/share/amp/threads/T-019c989e-1394-74ad-8234-ac573fcdb4c7.json +1260 -0
- package/.local/share/amp/threads/T-019c989f-e3dd-772e-85ac-525d0fc88fda.json +403 -0
- package/.local/share/amp/threads/T-019c98a1-7b0c-778a-b311-2e1cff85d710.json +3422 -0
- package/.local/share/amp/threads/T-019c98c5-4b7f-7284-99e9-88aa8c18ba66.json +1830 -0
- package/.local/share/amp/threads/T-019c98d0-f27f-76ec-be10-6df96f22be99.json +4061 -0
- package/.local/share/amp/threads/T-019c98f9-d031-704d-a0c2-f2f395f68f2b.json +509 -0
- package/.local/share/amp/threads/T-019c9919-f9ee-766c-90be-af7a07f6a4c6.json +2075 -0
- package/.local/share/amp/threads/T-019c991c-b98b-7158-9083-cc52408beb13.json +7 -0
- package/.local/share/amp/threads/T-019c991d-66d6-72aa-a9a1-105f7df0ea06.json +7 -0
- package/.local/share/amp/threads/T-019c9c2e-71a4-77ff-bd7f-b053da7f9000.json +1637 -0
- package/.local/share/amp/threads/T-019c9c45-27ca-728b-ba77-835115dfa9b2.json +3893 -0
- package/.local/share/amp/threads/T-019c9c48-45dc-736a-9752-e4119fe698f9.json +7 -0
- package/.local/share/amp/threads/T-019c9c4d-266b-72d0-b56e-74a5777e6583.json +7 -0
- package/.local/share/amp/threads/T-019c9c52-ab89-758f-9178-bda99c39d10b.json +7 -0
- package/.local/share/opencode/opencode.db +0 -0
- package/.local/share/opencode/opencode.db-shm +0 -0
- package/.local/share/opencode/opencode.db-wal +0 -0
- package/.local/share/opencode/storage/agent-usage-reminder/ses_36870ea98ffe8S5ZOCE4F11yFh.json +6 -0
- package/.local/share/opencode/storage/agent-usage-reminder/ses_3687a3e9affewUnHBzvpiPR6df.json +6 -0
- package/.local/share/opencode/storage/agent-usage-reminder/ses_36886e68dffeKVgUWf6lzXdEEt.json +6 -0
- package/.local/share/opencode/storage/session_diff/ses_36870ea98ffe8S5ZOCE4F11yFh.json +1 -0
- package/.local/share/opencode/storage/session_diff/ses_3687a3e9affewUnHBzvpiPR6df.json +1 -0
- package/.local/share/opencode/storage/session_diff/ses_36886e68dffeKVgUWf6lzXdEEt.json +1 -0
- package/.local/state/replit/log-query.db +0 -0
- package/.local/state/replit/log-query.db-shm +0 -0
- package/.local/state/replit/log-query.db-wal +0 -0
- package/.upm/store.json +1 -1
- package/AGENTS.md +32 -0
- package/bun.lock +137 -103
- package/index.jsx +24 -0
- package/package.json +9 -9
- package/src/agent.js +252 -169
- package/src/app.jsx +96 -0
- package/src/commands.js +66 -38
- package/src/components/AssistantMessage.jsx +83 -0
- package/src/components/ChatArea.jsx +84 -0
- package/src/components/DiffView.jsx +26 -0
- package/src/components/Divider.jsx +8 -0
- package/src/components/Header.jsx +44 -0
- package/src/components/HelpModal.jsx +81 -0
- package/src/components/InputBar.jsx +32 -0
- package/src/components/Spinner.jsx +23 -0
- package/src/components/StatusBar.jsx +44 -0
- package/src/components/SystemMessage.jsx +31 -0
- package/src/components/ThinkBlock.jsx +29 -0
- package/src/components/ToolCallItem.jsx +43 -0
- package/src/components/UserMessage.jsx +11 -0
- package/src/components/Welcome.jsx +14 -0
- package/src/config.js +118 -2
- package/src/hooks/useLayout.js +15 -0
- package/src/hooks/useStore.js +6 -0
- package/src/prompt.js +67 -48
- package/src/store.js +99 -0
- package/src/theme.js +19 -0
- package/src/thinking.js +0 -24
- package/src/toolExecutors.js +580 -23
- package/src/tools.js +146 -4
- package/src/utils.js +32 -0
- package/tsconfig.json +10 -0
- package/index.js +0 -92
- package/src/ui.js +0 -269
package/src/app.jsx
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { useKeyboard } from '@opentui/react';
|
|
3
|
+
import { useStore } from './hooks/useStore.js';
|
|
4
|
+
import { setState, addMessage, clearMessages, getRenderer } from './store.js';
|
|
5
|
+
import { session } from './config.js';
|
|
6
|
+
import { handleUserInput } from './agent.js';
|
|
7
|
+
import { handleSlashCommand } from './commands.js';
|
|
8
|
+
import Header from './components/Header.jsx';
|
|
9
|
+
import Divider from './components/Divider.jsx';
|
|
10
|
+
import ChatArea from './components/ChatArea.jsx';
|
|
11
|
+
import InputBar from './components/InputBar.jsx';
|
|
12
|
+
import StatusBar from './components/StatusBar.jsx';
|
|
13
|
+
import HelpModal from './components/HelpModal.jsx';
|
|
14
|
+
|
|
15
|
+
function exitApp() {
|
|
16
|
+
const renderer = getRenderer();
|
|
17
|
+
if (renderer) renderer.destroy();
|
|
18
|
+
|
|
19
|
+
const elapsed = ((Date.now() - session.startTime) / 1000 / 60).toFixed(1);
|
|
20
|
+
const parts = [
|
|
21
|
+
`${elapsed} min`,
|
|
22
|
+
`${session.turnCount} turns`,
|
|
23
|
+
`${session.toolCallCount} tool calls`,
|
|
24
|
+
`${session.totalTokens.toLocaleString()} tokens`,
|
|
25
|
+
`$${session.totalCost.toFixed(4)}`,
|
|
26
|
+
];
|
|
27
|
+
if (session.filesModified.size > 0) parts.push(`${session.filesModified.size} files modified`);
|
|
28
|
+
if (session.commandsRun.length > 0) parts.push(`${session.commandsRun.length} commands`);
|
|
29
|
+
|
|
30
|
+
console.log(`\n Session: ${parts.join(' · ')}\n`);
|
|
31
|
+
console.log(' Goodbye! ✦\n');
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default function App() {
|
|
36
|
+
const state = useStore();
|
|
37
|
+
|
|
38
|
+
useKeyboard((key) => {
|
|
39
|
+
if (key.ctrl && key.name === 'c') {
|
|
40
|
+
exitApp();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const handleInput = useCallback(async (value) => {
|
|
45
|
+
if (value === 'exit' || value === 'quit') {
|
|
46
|
+
exitApp();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (value.startsWith('/')) {
|
|
51
|
+
const result = await handleSlashCommand(value);
|
|
52
|
+
if (result?.action === 'quit') {
|
|
53
|
+
exitApp();
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
handleUserInput(value).catch(err => {
|
|
59
|
+
addMessage({ role: 'system', content: `Error: ${err.message}` });
|
|
60
|
+
setState({ isProcessing: false });
|
|
61
|
+
});
|
|
62
|
+
}, []);
|
|
63
|
+
|
|
64
|
+
const handleHelpCommand = useCallback((cmd) => {
|
|
65
|
+
if (cmd) {
|
|
66
|
+
handleSlashCommand(cmd).then(result => {
|
|
67
|
+
if (result?.action === 'quit') exitApp();
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<box style={{ flexDirection: 'column', flexGrow: 1 }}>
|
|
74
|
+
<Header />
|
|
75
|
+
<Divider />
|
|
76
|
+
<ChatArea
|
|
77
|
+
messages={state.messages}
|
|
78
|
+
streamingContent={state.streamingContent}
|
|
79
|
+
streamingThinking={state.streamingThinking}
|
|
80
|
+
isProcessing={state.isProcessing}
|
|
81
|
+
/>
|
|
82
|
+
<Divider />
|
|
83
|
+
<StatusBar isProcessing={state.isProcessing} />
|
|
84
|
+
<InputBar
|
|
85
|
+
disabled={state.isProcessing || state.showHelp}
|
|
86
|
+
onSubmit={handleInput}
|
|
87
|
+
/>
|
|
88
|
+
{state.showHelp ? (
|
|
89
|
+
<HelpModal
|
|
90
|
+
onClose={() => setState({ showHelp: false })}
|
|
91
|
+
onCommand={handleHelpCommand}
|
|
92
|
+
/>
|
|
93
|
+
) : null}
|
|
94
|
+
</box>
|
|
95
|
+
);
|
|
96
|
+
}
|
package/src/commands.js
CHANGED
|
@@ -3,103 +3,131 @@
|
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { execSync } = require('child_process');
|
|
6
|
-
const { PROJECT_ROOT, session, resolvePath } = require('./config');
|
|
7
|
-
const { t, indent, showHeader, showSessionSummary, showHelpMenu } = require('./ui');
|
|
6
|
+
const { PROJECT_ROOT, session, resolvePath, getMode, setMode } = require('./config');
|
|
8
7
|
const { executeTool } = require('./toolExecutors');
|
|
8
|
+
const store = require('./store');
|
|
9
9
|
|
|
10
|
-
async function handleSlashCommand(input
|
|
10
|
+
async function handleSlashCommand(input) {
|
|
11
11
|
const [cmd, ...rest] = input.split(' ');
|
|
12
12
|
const arg = rest.join(' ');
|
|
13
13
|
|
|
14
14
|
switch (cmd) {
|
|
15
15
|
case '/help':
|
|
16
|
-
|
|
16
|
+
store.setState({ showHelp: true });
|
|
17
17
|
break;
|
|
18
18
|
|
|
19
19
|
case '/clear':
|
|
20
20
|
session.conversationHistory = [];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
console.log();
|
|
21
|
+
store.clearMessages();
|
|
22
|
+
store.addMessage({ role: 'system', content: 'Conversation cleared.' });
|
|
24
23
|
break;
|
|
25
24
|
|
|
26
25
|
case '/files':
|
|
27
26
|
case '/ls': {
|
|
28
27
|
const dirPath = arg ? resolvePath(arg) : PROJECT_ROOT;
|
|
29
|
-
|
|
30
|
-
console.log(indent(t.text.bold('Project Files')));
|
|
31
|
-
console.log();
|
|
28
|
+
store.addMessage({ role: 'system', content: 'Loading file tree...', label: 'Project Files' });
|
|
32
29
|
const result = await executeTool('ListDir', { path: dirPath, recursive: true });
|
|
33
|
-
|
|
34
|
-
console.log();
|
|
30
|
+
store.addMessage({ role: 'system', content: result, label: 'Project Files' });
|
|
35
31
|
break;
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
case '/cost':
|
|
39
35
|
case '/status': {
|
|
40
36
|
const elapsed = ((Date.now() - session.startTime) / 1000 / 60).toFixed(1);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
37
|
+
const parts = [
|
|
38
|
+
`Session: ${elapsed} min`,
|
|
39
|
+
`Turns: ${session.turnCount}`,
|
|
40
|
+
`Tools: ${session.toolCallCount}`,
|
|
41
|
+
`Tokens: ${session.totalTokens.toLocaleString()}`,
|
|
42
|
+
`Cost: $${session.totalCost.toFixed(4)}`,
|
|
43
|
+
];
|
|
44
|
+
if (session.filesModified.size > 0) parts.push(`Files modified: ${session.filesModified.size}`);
|
|
45
|
+
if (session.commandsRun.length > 0) parts.push(`Commands: ${session.commandsRun.length}`);
|
|
46
|
+
store.addMessage({ role: 'system', content: parts.join('\n'), label: 'Session Stats' });
|
|
44
47
|
break;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
case '/undo': {
|
|
48
51
|
if (session.editHistory.length === 0) {
|
|
49
|
-
|
|
52
|
+
store.addMessage({ role: 'system', content: 'No edits to undo.' });
|
|
50
53
|
} else {
|
|
51
54
|
const last = session.editHistory[session.editHistory.length - 1];
|
|
52
55
|
fs.writeFileSync(last.path, last.before, 'utf-8');
|
|
53
56
|
session.editHistory.pop();
|
|
54
|
-
|
|
57
|
+
store.addMessage({ role: 'system', content: `Undone last edit to ${path.basename(last.path)}` });
|
|
55
58
|
}
|
|
56
|
-
console.log();
|
|
57
59
|
break;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
case '/diff': {
|
|
61
63
|
try {
|
|
62
64
|
const diff = execSync('git diff --stat 2>/dev/null', { encoding: 'utf-8', cwd: PROJECT_ROOT });
|
|
63
|
-
|
|
64
|
-
console.log(indent(t.text.bold('Git Diff')));
|
|
65
|
-
console.log(indent(t.dim(diff || '(no changes)'), 4));
|
|
66
|
-
console.log();
|
|
65
|
+
store.addMessage({ role: 'system', content: diff || '(no changes)', label: 'Git Diff' });
|
|
67
66
|
} catch {
|
|
68
|
-
|
|
69
|
-
console.log();
|
|
67
|
+
store.addMessage({ role: 'system', content: 'Not a git repository.' });
|
|
70
68
|
}
|
|
71
69
|
break;
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
case '/git': {
|
|
75
73
|
if (!arg) {
|
|
76
|
-
|
|
77
|
-
console.log();
|
|
74
|
+
store.addMessage({ role: 'system', content: 'Usage: /git <command>' });
|
|
78
75
|
break;
|
|
79
76
|
}
|
|
80
77
|
try {
|
|
81
78
|
const output = execSync(`git ${arg}`, { encoding: 'utf-8', cwd: PROJECT_ROOT });
|
|
82
|
-
|
|
83
|
-
console.log(indent(t.dim(output), 4));
|
|
84
|
-
console.log();
|
|
79
|
+
store.addMessage({ role: 'system', content: output || '(no output)', label: `git ${arg}` });
|
|
85
80
|
} catch (err) {
|
|
86
|
-
|
|
87
|
-
|
|
81
|
+
store.addMessage({ role: 'system', content: err.stderr || err.message });
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case '/mode': {
|
|
87
|
+
if (!arg) {
|
|
88
|
+
const current = getMode();
|
|
89
|
+
const modes = ['default', 'max', 'lite'];
|
|
90
|
+
const modeDescriptions = {
|
|
91
|
+
default: 'Single agent pass with auto code review',
|
|
92
|
+
max: 'Multi-strategy editing, best-of-N thinking, multi-perspective review, auto context pruning',
|
|
93
|
+
lite: 'Fast mode — skips validation and review steps',
|
|
94
|
+
};
|
|
95
|
+
const lines = modes.map(m =>
|
|
96
|
+
`${m === current ? '→ ' : ' '}${m.padEnd(10)} ${modeDescriptions[m]}`
|
|
97
|
+
);
|
|
98
|
+
store.addMessage({ role: 'system', content: `Current mode: ${current}\n\n${lines.join('\n')}`, label: 'Mode' });
|
|
99
|
+
} else {
|
|
100
|
+
const success = setMode(arg.trim());
|
|
101
|
+
if (success) {
|
|
102
|
+
store.addMessage({ role: 'system', content: `Mode set to: ${arg.trim()}`, label: 'Mode' });
|
|
103
|
+
} else {
|
|
104
|
+
store.addMessage({ role: 'system', content: `Invalid mode: ${arg}. Use default, max, or lite.` });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case '/compact': {
|
|
111
|
+
const pruneId = store.addMessage({ role: 'system', content: 'Compacting conversation...', label: 'Context Pruner' });
|
|
112
|
+
try {
|
|
113
|
+
const result = await executeTool('ContextPruner', {}, (partial) => {
|
|
114
|
+
store.updateMessage(pruneId, { content: partial, label: 'Context Pruner' });
|
|
115
|
+
});
|
|
116
|
+
store.updateMessage(pruneId, { content: result, label: 'Context Pruner' });
|
|
117
|
+
} catch (err) {
|
|
118
|
+
store.updateMessage(pruneId, { content: `Compaction failed: ${err.message}` });
|
|
88
119
|
}
|
|
89
120
|
break;
|
|
90
121
|
}
|
|
91
122
|
|
|
92
123
|
case '/quit':
|
|
93
|
-
|
|
94
|
-
showSessionSummary();
|
|
95
|
-
console.log(indent(t.dim('Goodbye! ') + t.primary('✦')));
|
|
96
|
-
console.log();
|
|
97
|
-
process.exit(0);
|
|
124
|
+
return { action: 'quit' };
|
|
98
125
|
|
|
99
126
|
default:
|
|
100
|
-
|
|
101
|
-
console.log();
|
|
127
|
+
store.addMessage({ role: 'system', content: `Unknown command: ${cmd}. Type /help for available commands.` });
|
|
102
128
|
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
103
131
|
}
|
|
104
132
|
|
|
105
133
|
module.exports = { handleSlashCommand };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { TextAttributes } from '@opentui/core';
|
|
2
|
+
import { colors } from '../theme.js';
|
|
3
|
+
import { useLayout } from '../hooks/useLayout.js';
|
|
4
|
+
|
|
5
|
+
export default function AssistantMessage({ content, isStreaming }) {
|
|
6
|
+
const { indent, isNarrow, width } = useLayout();
|
|
7
|
+
const codeIndent = isNarrow ? 1 : 2;
|
|
8
|
+
const separatorWidth = Math.min(width - indent - codeIndent, isNarrow ? 40 : 60);
|
|
9
|
+
if (!content) return null;
|
|
10
|
+
|
|
11
|
+
const lines = content.split('\n');
|
|
12
|
+
const rendered = [];
|
|
13
|
+
let inCodeBlock = false;
|
|
14
|
+
let codeLines = [];
|
|
15
|
+
let codeLang = '';
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
const line = lines[i];
|
|
19
|
+
|
|
20
|
+
if (line.startsWith('```') && !inCodeBlock) {
|
|
21
|
+
inCodeBlock = true;
|
|
22
|
+
codeLang = line.slice(3).trim() || 'code';
|
|
23
|
+
codeLines = [];
|
|
24
|
+
} else if (line.startsWith('```') && inCodeBlock) {
|
|
25
|
+
inCodeBlock = false;
|
|
26
|
+
rendered.push(
|
|
27
|
+
<box key={`code-${i}`} style={{ flexDirection: 'column', paddingLeft: codeIndent, marginTop: 0 }}>
|
|
28
|
+
<text fg={colors.dim} content={`── ${codeLang} ──`} />
|
|
29
|
+
{codeLines.map((cl, j) => (
|
|
30
|
+
<text key={j}>
|
|
31
|
+
<span fg={colors.dim}>{String(j + 1).padStart(isNarrow ? 2 : 3) + ' │ '}</span>
|
|
32
|
+
<span fg={colors.text}>{cl}</span>
|
|
33
|
+
</text>
|
|
34
|
+
))}
|
|
35
|
+
<text fg={colors.dim} content={'─'.repeat(Math.max(separatorWidth, 10))} />
|
|
36
|
+
</box>
|
|
37
|
+
);
|
|
38
|
+
} else if (inCodeBlock) {
|
|
39
|
+
codeLines.push(line);
|
|
40
|
+
} else {
|
|
41
|
+
const processed = line.replace(/`([^`]+)`/g, '«$1»');
|
|
42
|
+
if (processed.includes('«')) {
|
|
43
|
+
const parts = processed.split(/«|»/);
|
|
44
|
+
rendered.push(
|
|
45
|
+
<text key={`line-${i}`}>
|
|
46
|
+
{parts.map((part, j) =>
|
|
47
|
+
j % 2 === 0
|
|
48
|
+
? <span key={j} fg={colors.text}>{part}</span>
|
|
49
|
+
: <span key={j} fg={colors.cyan}>{part}</span>
|
|
50
|
+
)}
|
|
51
|
+
</text>
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
rendered.push(
|
|
55
|
+
<text key={`line-${i}`}>
|
|
56
|
+
<span fg={colors.text}>{line}</span>
|
|
57
|
+
</text>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (inCodeBlock && codeLines.length > 0) {
|
|
64
|
+
rendered.push(
|
|
65
|
+
<box key="code-tail" style={{ flexDirection: 'column', paddingLeft: codeIndent }}>
|
|
66
|
+
<text fg={colors.dim} content={`── ${codeLang} ──`} />
|
|
67
|
+
{codeLines.map((cl, j) => (
|
|
68
|
+
<text key={j}>
|
|
69
|
+
<span fg={colors.dim}>{String(j + 1).padStart(isNarrow ? 2 : 3) + ' │ '}</span>
|
|
70
|
+
<span fg={colors.text}>{cl}</span>
|
|
71
|
+
</text>
|
|
72
|
+
))}
|
|
73
|
+
</box>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<box style={{ flexDirection: 'column', paddingLeft: indent }}>
|
|
79
|
+
{rendered}
|
|
80
|
+
{isStreaming ? <text fg={colors.accent} content="▊" /> : null}
|
|
81
|
+
</box>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { TextAttributes } from '@opentui/core';
|
|
2
|
+
import { colors } from '../theme.js';
|
|
3
|
+
import { useLayout } from '../hooks/useLayout.js';
|
|
4
|
+
import Welcome from './Welcome.jsx';
|
|
5
|
+
import UserMessage from './UserMessage.jsx';
|
|
6
|
+
import AssistantMessage from './AssistantMessage.jsx';
|
|
7
|
+
import ThinkBlock from './ThinkBlock.jsx';
|
|
8
|
+
import ToolCallItem from './ToolCallItem.jsx';
|
|
9
|
+
import DiffView from './DiffView.jsx';
|
|
10
|
+
import SystemMessage from './SystemMessage.jsx';
|
|
11
|
+
import Spinner from './Spinner.jsx';
|
|
12
|
+
import { toggleMessageExpanded } from '../store.js';
|
|
13
|
+
|
|
14
|
+
function MessageItem({ message }) {
|
|
15
|
+
const { width } = useLayout();
|
|
16
|
+
switch (message.role) {
|
|
17
|
+
case 'user':
|
|
18
|
+
return <UserMessage content={message.content} />;
|
|
19
|
+
case 'assistant':
|
|
20
|
+
return (
|
|
21
|
+
<box style={{ flexDirection: 'column', marginTop: 1 }}>
|
|
22
|
+
<text fg={colors.primary} attributes={TextAttributes.BOLD} style={{ paddingLeft: 1 }} content="Apex" />
|
|
23
|
+
<AssistantMessage content={message.content} />
|
|
24
|
+
</box>
|
|
25
|
+
);
|
|
26
|
+
case 'thinking':
|
|
27
|
+
return (
|
|
28
|
+
<ThinkBlock
|
|
29
|
+
content={message.content}
|
|
30
|
+
expanded={message.expanded}
|
|
31
|
+
onToggle={() => toggleMessageExpanded(message.id)}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
case 'tool':
|
|
35
|
+
return <ToolCallItem message={message} />;
|
|
36
|
+
case 'diff':
|
|
37
|
+
return <DiffView filename={message.filename} content={message.content} />;
|
|
38
|
+
case 'system':
|
|
39
|
+
return <SystemMessage message={message} />;
|
|
40
|
+
case 'divider':
|
|
41
|
+
return <text fg={colors.dim} style={{ paddingLeft: 1 }} content={'─'.repeat(Math.max(width - 2, 10))} />;
|
|
42
|
+
default:
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export default function ChatArea({ messages, streamingContent, streamingThinking, isProcessing }) {
|
|
48
|
+
const { indent } = useLayout();
|
|
49
|
+
return (
|
|
50
|
+
<scrollbox
|
|
51
|
+
style={{ flexGrow: 1 }}
|
|
52
|
+
focused
|
|
53
|
+
stickyScroll
|
|
54
|
+
stickyStart="bottom"
|
|
55
|
+
scrollY
|
|
56
|
+
>
|
|
57
|
+
<box style={{ flexDirection: 'column' }}>
|
|
58
|
+
<Welcome />
|
|
59
|
+
{messages.map(msg => (
|
|
60
|
+
<MessageItem key={msg.id} message={msg} />
|
|
61
|
+
))}
|
|
62
|
+
{streamingThinking ? (
|
|
63
|
+
<box style={{ paddingLeft: 2, marginTop: 0 }}>
|
|
64
|
+
<text fg={colors.dim} attributes={TextAttributes.ITALIC}>
|
|
65
|
+
<span fg={colors.dim}>{'▸ Thinking: '}</span>
|
|
66
|
+
<span fg={colors.dim}>{streamingThinking.slice(-100)}</span>
|
|
67
|
+
</text>
|
|
68
|
+
</box>
|
|
69
|
+
) : null}
|
|
70
|
+
{streamingContent ? (
|
|
71
|
+
<box style={{ flexDirection: 'column', marginTop: 0 }}>
|
|
72
|
+
<AssistantMessage content={streamingContent} isStreaming />
|
|
73
|
+
</box>
|
|
74
|
+
) : null}
|
|
75
|
+
{isProcessing && !streamingContent && !streamingThinking ? (
|
|
76
|
+
<box style={{ paddingLeft: indent, marginTop: 1 }}>
|
|
77
|
+
<Spinner label="Reasoning..." />
|
|
78
|
+
</box>
|
|
79
|
+
) : null}
|
|
80
|
+
<box style={{ height: 1 }} />
|
|
81
|
+
</box>
|
|
82
|
+
</scrollbox>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { TextAttributes } from '@opentui/core';
|
|
2
|
+
import { colors } from '../theme.js';
|
|
3
|
+
import { useLayout } from '../hooks/useLayout.js';
|
|
4
|
+
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
export default function DiffView({ filename, content }) {
|
|
8
|
+
const { indent } = useLayout();
|
|
9
|
+
if (!content) return null;
|
|
10
|
+
const lines = content.split('\n');
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<box style={{ flexDirection: 'column', paddingLeft: indent }}>
|
|
14
|
+
<text fg={colors.text} attributes={TextAttributes.BOLD} content={path.basename(filename || '')} />
|
|
15
|
+
{lines.map((line, i) => {
|
|
16
|
+
if (line.startsWith('+')) {
|
|
17
|
+
return <text key={i} fg={colors.green} content={line} />;
|
|
18
|
+
}
|
|
19
|
+
if (line.startsWith('-')) {
|
|
20
|
+
return <text key={i} fg={colors.red} content={line} />;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
})}
|
|
24
|
+
</box>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { colors } from '../theme.js';
|
|
2
|
+
import { useLayout } from '../hooks/useLayout.js';
|
|
3
|
+
|
|
4
|
+
export default function Divider() {
|
|
5
|
+
const { width } = useLayout();
|
|
6
|
+
const cols = Math.min(width, 120);
|
|
7
|
+
return <text fg={colors.dim} content={'─'.repeat(cols)} />;
|
|
8
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { TextAttributes } from '@opentui/core';
|
|
3
|
+
import { colors } from '../theme.js';
|
|
4
|
+
import { PROJECT_ROOT, getMode } from '../config.js';
|
|
5
|
+
import { useLayout } from '../hooks/useLayout.js';
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
|
|
10
|
+
export default function Header() {
|
|
11
|
+
const [branch, setBranch] = useState('');
|
|
12
|
+
const { isNarrow } = useLayout();
|
|
13
|
+
const cwd = path.basename(PROJECT_ROOT);
|
|
14
|
+
const mode = getMode();
|
|
15
|
+
const modeColors = { default: colors.dim, max: colors.accent, lite: colors.muted };
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
try {
|
|
19
|
+
const b = execSync('git rev-parse --abbrev-ref HEAD 2>/dev/null', {
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
cwd: PROJECT_ROOT,
|
|
22
|
+
}).trim();
|
|
23
|
+
setBranch(b);
|
|
24
|
+
} catch {}
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<box style={{ flexDirection: 'row', paddingLeft: 1, paddingRight: 1 }}>
|
|
29
|
+
<text>
|
|
30
|
+
<span fg={colors.primary} attributes={TextAttributes.BOLD}>⚡ Apex</span>
|
|
31
|
+
<span fg={colors.dim}>{' '}</span>
|
|
32
|
+
<span fg={modeColors[mode] || colors.dim}>[{mode}]</span>
|
|
33
|
+
<span fg={colors.dim}>{' '}</span>
|
|
34
|
+
<span fg={colors.muted}>{isNarrow && cwd.length > 12 ? cwd.slice(0, 12) + '…' : cwd}</span>
|
|
35
|
+
{branch && !isNarrow ? (
|
|
36
|
+
<>
|
|
37
|
+
<span fg={colors.dim}>{' on '}</span>
|
|
38
|
+
<span fg={colors.text}>{branch}</span>
|
|
39
|
+
</>
|
|
40
|
+
) : null}
|
|
41
|
+
</text>
|
|
42
|
+
</box>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { TextAttributes } from '@opentui/core';
|
|
2
|
+
import { useKeyboard } from '@opentui/react';
|
|
3
|
+
import { colors } from '../theme.js';
|
|
4
|
+
import { useLayout } from '../hooks/useLayout.js';
|
|
5
|
+
|
|
6
|
+
const COMMANDS = [
|
|
7
|
+
{ cmd: '/help', desc: 'Show this menu' },
|
|
8
|
+
{ cmd: '/mode', desc: 'Show/set mode (default, max, lite)' },
|
|
9
|
+
{ cmd: '/compact', desc: 'Compact conversation context' },
|
|
10
|
+
{ cmd: '/files', desc: 'Show project file tree' },
|
|
11
|
+
{ cmd: '/clear', desc: 'Clear conversation' },
|
|
12
|
+
{ cmd: '/cost', desc: 'Show session stats' },
|
|
13
|
+
{ cmd: '/undo', desc: 'Undo last edit' },
|
|
14
|
+
{ cmd: '/diff', desc: 'Show git diff' },
|
|
15
|
+
{ cmd: '/git <cmd>', desc: 'Run a git command' },
|
|
16
|
+
{ cmd: '/quit', desc: 'Exit' },
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const TOOLS = [
|
|
20
|
+
'Read', 'Write', 'Edit', 'Patch', 'Bash', 'Grep', 'Glob', 'ListDir', 'UndoEdit', 'Task', 'CodeReview',
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const SUBAGENTS = [
|
|
24
|
+
'FilePickerMax', 'Thinker', 'ThinkerBestOfN*', 'EditorMultiPrompt*', 'CodeReviewMulti*', 'Commander', 'ContextPruner',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export default function HelpModal({ onClose, onCommand }) {
|
|
28
|
+
const { isNarrow } = useLayout();
|
|
29
|
+
|
|
30
|
+
useKeyboard((key) => {
|
|
31
|
+
if (key.name === 'escape' || key.name === 'q') {
|
|
32
|
+
onClose();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<box
|
|
38
|
+
zIndex={100}
|
|
39
|
+
border
|
|
40
|
+
borderColor={colors.primary}
|
|
41
|
+
backgroundColor="#0d0d1a"
|
|
42
|
+
title=" Help "
|
|
43
|
+
titleAlignment="center"
|
|
44
|
+
style={{
|
|
45
|
+
position: 'absolute',
|
|
46
|
+
top: 2,
|
|
47
|
+
left: isNarrow ? 1 : 4,
|
|
48
|
+
bottom: 2,
|
|
49
|
+
right: isNarrow ? 1 : 4,
|
|
50
|
+
padding: 1,
|
|
51
|
+
flexDirection: 'column',
|
|
52
|
+
}}
|
|
53
|
+
>
|
|
54
|
+
<text fg={colors.white} attributes={TextAttributes.BOLD} content="Commands" />
|
|
55
|
+
<box style={{ flexDirection: 'column', marginTop: 0 }}>
|
|
56
|
+
{COMMANDS.map(({ cmd, desc }) => (
|
|
57
|
+
<box
|
|
58
|
+
key={cmd}
|
|
59
|
+
style={{ flexDirection: 'row' }}
|
|
60
|
+
onMouseDown={() => {
|
|
61
|
+
const slashCmd = cmd.split(' ')[0];
|
|
62
|
+
if (onCommand && !cmd.includes('<')) onCommand(slashCmd);
|
|
63
|
+
onClose();
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
<text>
|
|
67
|
+
<span fg={colors.accent}>{cmd.padEnd(isNarrow ? 10 : 14)}</span>
|
|
68
|
+
<span fg={colors.text}>{desc}</span>
|
|
69
|
+
</text>
|
|
70
|
+
</box>
|
|
71
|
+
))}
|
|
72
|
+
</box>
|
|
73
|
+
<text fg={colors.white} attributes={TextAttributes.BOLD} style={{ marginTop: 1 }} content="Tools" />
|
|
74
|
+
<text fg={colors.dim} content={TOOLS.join(', ')} />
|
|
75
|
+
<text fg={colors.white} attributes={TextAttributes.BOLD} style={{ marginTop: 1 }} content="Sub-Agents" />
|
|
76
|
+
<text fg={colors.dim} content={SUBAGENTS.join(', ')} />
|
|
77
|
+
<text fg={colors.dim} content=" * = MAX mode only" />
|
|
78
|
+
<text fg={colors.dim} style={{ marginTop: 1 }} content="Press ESC or q to close" />
|
|
79
|
+
</box>
|
|
80
|
+
);
|
|
81
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { useRef } from 'react';
|
|
2
|
+
import { colors } from '../theme.js';
|
|
3
|
+
import { useLayout } from '../hooks/useLayout.js';
|
|
4
|
+
|
|
5
|
+
export default function InputBar({ disabled, onSubmit }) {
|
|
6
|
+
const inputRef = useRef(null);
|
|
7
|
+
const { isNarrow } = useLayout();
|
|
8
|
+
|
|
9
|
+
const handleSubmit = (value) => {
|
|
10
|
+
const trimmed = value.trim();
|
|
11
|
+
if (!trimmed) return;
|
|
12
|
+
if (inputRef.current) inputRef.current.value = '';
|
|
13
|
+
onSubmit(trimmed);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<box style={{ flexDirection: 'column' }}>
|
|
18
|
+
<text fg={colors.dim} style={{ paddingLeft: isNarrow ? 1 : 2 }} content={isNarrow ? '^C exit · /help' : 'Ctrl+C to exit · /help for commands'} />
|
|
19
|
+
<box style={{ flexDirection: 'row', paddingLeft: 1 }}>
|
|
20
|
+
<text fg={colors.primary} content="❯ " />
|
|
21
|
+
<input
|
|
22
|
+
ref={inputRef}
|
|
23
|
+
focused={!disabled}
|
|
24
|
+
placeholder={disabled ? 'Processing...' : 'Type a message...'}
|
|
25
|
+
onSubmit={handleSubmit}
|
|
26
|
+
fg={colors.text}
|
|
27
|
+
style={{ flexGrow: 1 }}
|
|
28
|
+
/>
|
|
29
|
+
</box>
|
|
30
|
+
</box>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { colors } from '../theme.js';
|
|
3
|
+
|
|
4
|
+
const FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
5
|
+
|
|
6
|
+
export default function Spinner({ label }) {
|
|
7
|
+
const [frame, setFrame] = useState(0);
|
|
8
|
+
const timerRef = useRef(null);
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
timerRef.current = setInterval(() => {
|
|
12
|
+
setFrame(f => (f + 1) % FRAMES.length);
|
|
13
|
+
}, 80);
|
|
14
|
+
return () => clearInterval(timerRef.current);
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<text>
|
|
19
|
+
<span fg={colors.accent}>{FRAMES[frame]}</span>
|
|
20
|
+
{label ? <span fg={colors.dim}>{' ' + label}</span> : null}
|
|
21
|
+
</text>
|
|
22
|
+
);
|
|
23
|
+
}
|