@jwcode/cli 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/README.md +348 -0
- package/backend/.gitkeep +0 -0
- package/dist/__tests__/store.test.d.ts +1 -0
- package/dist/__tests__/store.test.js +43 -0
- package/dist/__tests__/theme.test.d.ts +1 -0
- package/dist/__tests__/theme.test.js +40 -0
- package/dist/__tests__/tokenEstimate.test.d.ts +1 -0
- package/dist/__tests__/tokenEstimate.test.js +48 -0
- package/dist/cli.js +83 -0
- package/dist/commands/index.d.ts +18 -0
- package/dist/commands/index.js +99 -0
- package/dist/components/ApprovalModal.d.ts +8 -0
- package/dist/components/ApprovalModal.js +47 -0
- package/dist/components/ChatArea.d.ts +10 -0
- package/dist/components/ChatArea.js +49 -0
- package/dist/components/CommandPalette.d.ts +6 -0
- package/dist/components/CommandPalette.js +72 -0
- package/dist/components/StatusLine.d.ts +1 -0
- package/dist/components/StatusLine.js +25 -0
- package/dist/components/TextInput.d.ts +10 -0
- package/dist/components/TextInput.js +118 -0
- package/dist/hooks/useAppState.d.ts +18 -0
- package/dist/hooks/useAppState.js +42 -0
- package/dist/hooks/useWebSocket.d.ts +3 -0
- package/dist/hooks/useWebSocket.js +8 -0
- package/package.json +43 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* CommandPalette — / filterable popup.
|
|
4
|
+
* Character input is handled by TextInput; this only handles navigation.
|
|
5
|
+
*/
|
|
6
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
7
|
+
import { Box, Text, useInput, useStdout } from 'ink';
|
|
8
|
+
import { ALL_COMMANDS } from '../commands/index.js';
|
|
9
|
+
export function CommandPalette({ filter, onSelect }) {
|
|
10
|
+
const [selected, setSelected] = useState(0);
|
|
11
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
12
|
+
const { stdout } = useStdout();
|
|
13
|
+
const terminalRows = stdout?.rows || 24;
|
|
14
|
+
const visible = useMemo(() => {
|
|
15
|
+
const f = filter.replace(/^\//, '').toLowerCase();
|
|
16
|
+
if (!f)
|
|
17
|
+
return ALL_COMMANDS;
|
|
18
|
+
return ALL_COMMANDS.filter(c => c.cmd.toLowerCase().includes(f) || c.desc.includes(f));
|
|
19
|
+
}, [filter]);
|
|
20
|
+
useEffect(() => { setSelected(0); setScrollOffset(0); }, [filter]);
|
|
21
|
+
const maxShow = Math.max(5, terminalRows - 13);
|
|
22
|
+
// Keep selected row in view
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
setScrollOffset(prev => {
|
|
25
|
+
if (selected < prev)
|
|
26
|
+
return selected;
|
|
27
|
+
if (selected >= prev + maxShow)
|
|
28
|
+
return selected - maxShow + 1;
|
|
29
|
+
return prev;
|
|
30
|
+
});
|
|
31
|
+
}, [selected, maxShow]);
|
|
32
|
+
const sliced = visible.slice(scrollOffset, scrollOffset + maxShow);
|
|
33
|
+
useInput((_input, key) => {
|
|
34
|
+
if (key.escape) {
|
|
35
|
+
onSelect(null);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (key.downArrow) {
|
|
39
|
+
setSelected(prev => Math.min(prev + 1, visible.length - 1));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (key.upArrow) {
|
|
43
|
+
setSelected(prev => Math.max(prev - 1, 0));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (key.pageDown) {
|
|
47
|
+
setSelected(prev => Math.min(prev + maxShow, visible.length - 1));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (key.pageUp) {
|
|
51
|
+
setSelected(prev => Math.max(prev - maxShow, 0));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (key.home) {
|
|
55
|
+
setSelected(0);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (key.end) {
|
|
59
|
+
setSelected(visible.length - 1);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (key.return) {
|
|
63
|
+
if (visible.length > 0 && selected >= 0 && selected < visible.length) {
|
|
64
|
+
onSelect(visible[selected].cmd);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "single", borderColor: "cyan", paddingX: 1, width: 52, children: [_jsxs(Box, { children: [_jsx(Text, { bold: true, color: "cyan", children: "\u547D\u4EE4\u5217\u8868" }), _jsx(Text, { dimColor: true, children: " \u2191\u2193\u9009\u62E9 / PgUp/PgDn\u7FFB\u9875 / \u56DE\u8F66\u786E\u8BA4 / Esc\u53D6\u6D88" })] }), sliced.map((cmd, i) => {
|
|
69
|
+
const idx = scrollOffset + i;
|
|
70
|
+
return (_jsxs(Box, { paddingLeft: 1, children: [_jsx(Text, { color: idx === selected ? 'cyan' : undefined, bold: idx === selected, children: idx === selected ? '> ' : ' ' }), _jsx(Text, { color: "green", children: cmd.cmd }), _jsxs(Text, { dimColor: true, children: [" ", cmd.desc] }), _jsxs(Text, { color: cmd.via === 'ws' ? 'yellow' : 'blue', dimColor: idx !== selected, children: ["(", cmd.via === 'ws' ? '后端' : '本地', ")"] })] }, cmd.cmd));
|
|
71
|
+
}), visible.length > maxShow && (_jsx(Box, { children: _jsxs(Text, { dimColor: true, children: [" ", scrollOffset + 1, "-", Math.min(scrollOffset + maxShow, visible.length), " / ", visible.length] }) }))] }));
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function StatusLine(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { useAppState } from '../hooks/useAppState.js';
|
|
4
|
+
function formatTokens(n) {
|
|
5
|
+
if (n >= 1_000_000)
|
|
6
|
+
return `${(n / 1_000_000).toFixed(1)}M`;
|
|
7
|
+
if (n >= 1_000)
|
|
8
|
+
return `${Math.round(n / 1_000)}K`;
|
|
9
|
+
return String(n);
|
|
10
|
+
}
|
|
11
|
+
export function StatusLine() {
|
|
12
|
+
const state = useAppState();
|
|
13
|
+
const { usage, modelName, planMode, autoMode, connected, statusText, messages } = state;
|
|
14
|
+
const msgCount = messages.length;
|
|
15
|
+
const pct = Math.min(100, Math.round(usage.usageRatio * 100));
|
|
16
|
+
const filled = Math.round(pct / 10);
|
|
17
|
+
const bar = '='.repeat(filled) + '-'.repeat(10 - filled);
|
|
18
|
+
const model = modelName || (connected ? 'ready' : 'connecting...');
|
|
19
|
+
const modeLabel = planMode ? ' Plan ' : ' Act ';
|
|
20
|
+
const modeColor = planMode ? 'cyan' : 'green';
|
|
21
|
+
const connIcon = connected ? '●' : '○';
|
|
22
|
+
const connColor = connected ? 'green' : 'red';
|
|
23
|
+
const isError = statusText.startsWith('Error:');
|
|
24
|
+
return (_jsxs(Box, { flexDirection: "column", width: "100%", paddingRight: 1, children: [_jsxs(Box, { height: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "jwcode" }), _jsx(Text, { children: " " }), _jsxs(Text, { backgroundColor: modeColor, color: "black", children: [" ", modeLabel, " "] }), _jsx(Text, { children: " " }), autoMode && (_jsxs(_Fragment, { children: [_jsx(Text, { backgroundColor: "magenta", color: "black", children: " AUTO " }), _jsx(Text, { children: " " })] })), _jsxs(Text, { color: connColor, children: [connIcon, " "] }), _jsx(Text, { color: "green", children: model }), _jsx(Text, { children: " " }), _jsxs(Text, { dimColor: true, children: [msgCount, "msgs"] }), _jsx(Text, { children: " t: " }), _jsx(Text, { color: "yellow", children: formatTokens(usage.totalTokens) }), _jsx(Text, { children: " " }), _jsxs(Text, { color: pct > 90 ? 'red' : 'white', children: [bar, " ", pct, "%"] })] }), statusText && statusText !== 'connecting...' && (_jsx(Box, { height: 1, children: _jsx(Text, { color: isError ? 'red' : 'grey', dimColor: !isError, children: statusText.slice(0, 100) }) }))] }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare function saveToHistory(text: string): void;
|
|
2
|
+
interface Props {
|
|
3
|
+
value: string;
|
|
4
|
+
onChange: (value: string) => void;
|
|
5
|
+
onSubmit: (value: string) => void;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
disabled?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function TextInput({ value, onChange, onSubmit, placeholder, disabled }: Props): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useCallback } from 'react';
|
|
3
|
+
import { Box, Text, useInput } from 'ink';
|
|
4
|
+
// Rough token estimation: English ~4 chars/token, CJK ~1.5 chars/token
|
|
5
|
+
function estimateTokens(text) {
|
|
6
|
+
let cjk = 0;
|
|
7
|
+
let other = 0;
|
|
8
|
+
for (const ch of text) {
|
|
9
|
+
if (/[一-鿿㐀-䶿豈- -〿-]/.test(ch)) {
|
|
10
|
+
cjk++;
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
other++;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return Math.ceil(cjk / 1.5 + other / 4);
|
|
17
|
+
}
|
|
18
|
+
const MAX_HISTORY = 30;
|
|
19
|
+
const HISTORY_KEY = 'jwcode-tscli-history';
|
|
20
|
+
function loadHistory() {
|
|
21
|
+
try {
|
|
22
|
+
const raw = process.env.JWCODE_HISTORY
|
|
23
|
+
|| (typeof sessionStorage !== 'undefined' ? sessionStorage.getItem(HISTORY_KEY) : null);
|
|
24
|
+
return raw ? JSON.parse(raw) : [];
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function saveHistory(entries) {
|
|
31
|
+
try {
|
|
32
|
+
if (typeof sessionStorage !== 'undefined') {
|
|
33
|
+
sessionStorage.setItem(HISTORY_KEY, JSON.stringify(entries));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch { /* ignore */ }
|
|
37
|
+
}
|
|
38
|
+
export function saveToHistory(text) {
|
|
39
|
+
const trimmed = text.trim();
|
|
40
|
+
if (!trimmed)
|
|
41
|
+
return;
|
|
42
|
+
const history = loadHistory().filter(h => h !== trimmed);
|
|
43
|
+
history.unshift(trimmed);
|
|
44
|
+
saveHistory(history.slice(0, MAX_HISTORY));
|
|
45
|
+
}
|
|
46
|
+
export function TextInput({ value, onChange, onSubmit, placeholder, disabled }) {
|
|
47
|
+
const historyRef = useRef(loadHistory());
|
|
48
|
+
const histIdxRef = useRef(-1);
|
|
49
|
+
const draftRef = useRef('');
|
|
50
|
+
const navigateHistory = useCallback((dir) => {
|
|
51
|
+
const history = historyRef.current;
|
|
52
|
+
if (history.length === 0)
|
|
53
|
+
return null;
|
|
54
|
+
if (histIdxRef.current === -1) {
|
|
55
|
+
draftRef.current = value;
|
|
56
|
+
if (dir === 'up') {
|
|
57
|
+
histIdxRef.current = 0;
|
|
58
|
+
return history[0];
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (dir === 'up') {
|
|
63
|
+
const next = Math.min(histIdxRef.current + 1, history.length - 1);
|
|
64
|
+
histIdxRef.current = next;
|
|
65
|
+
return history[next];
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
const next = histIdxRef.current - 1;
|
|
69
|
+
if (next < 0) {
|
|
70
|
+
histIdxRef.current = -1;
|
|
71
|
+
return draftRef.current;
|
|
72
|
+
}
|
|
73
|
+
histIdxRef.current = next;
|
|
74
|
+
return history[next];
|
|
75
|
+
}
|
|
76
|
+
}, [value]);
|
|
77
|
+
const resetHistory = useCallback(() => {
|
|
78
|
+
histIdxRef.current = -1;
|
|
79
|
+
draftRef.current = '';
|
|
80
|
+
}, []);
|
|
81
|
+
useInput((input, key) => {
|
|
82
|
+
if (disabled)
|
|
83
|
+
return;
|
|
84
|
+
if (key.return) {
|
|
85
|
+
onSubmit(value);
|
|
86
|
+
resetHistory();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (key.upArrow) {
|
|
90
|
+
const hist = navigateHistory('up');
|
|
91
|
+
if (hist !== null)
|
|
92
|
+
onChange(hist);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (key.downArrow) {
|
|
96
|
+
const hist = navigateHistory('down');
|
|
97
|
+
if (hist !== null)
|
|
98
|
+
onChange(hist);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Any manual edit resets history navigation
|
|
102
|
+
if (histIdxRef.current !== -1 && input) {
|
|
103
|
+
resetHistory();
|
|
104
|
+
}
|
|
105
|
+
if (key.backspace || key.delete) {
|
|
106
|
+
onChange(value.slice(0, -1));
|
|
107
|
+
resetHistory();
|
|
108
|
+
}
|
|
109
|
+
else if (input && !key.ctrl && !key.meta && !key.tab && !key.escape) {
|
|
110
|
+
onChange(value + input);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const display = value || '';
|
|
114
|
+
const showPlaceholder = !display && placeholder;
|
|
115
|
+
const tokenEstimate = display ? estimateTokens(display) : 0;
|
|
116
|
+
const charCount = display.length;
|
|
117
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [display ? _jsx(Text, { children: display }) : _jsx(Text, { dimColor: true, children: placeholder }), _jsx(Text, { dimColor: true, children: "\u258A" })] }), charCount > 0 && (_jsxs(Box, { children: [_jsxs(Text, { dimColor: true, children: [" ", charCount, " \u5B57\u7B26 \u2248 ", tokenEstimate, " tokens"] }), tokenEstimate > 100000 && (_jsx(Text, { color: "red", children: " \u26A0 \u63A5\u8FD1\u4E0A\u4E0B\u6587\u4E0A\u9650" }))] }))] }));
|
|
118
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type Store } from '../store.js';
|
|
2
|
+
import type { Message, TokenUsage } from '../protocol.js';
|
|
3
|
+
export interface AppState {
|
|
4
|
+
messages: Message[];
|
|
5
|
+
currentMessage: Message | null;
|
|
6
|
+
usage: TokenUsage;
|
|
7
|
+
planMode: boolean;
|
|
8
|
+
autoMode: boolean;
|
|
9
|
+
planWaiting: boolean;
|
|
10
|
+
scrollOffset: number;
|
|
11
|
+
modelName: string;
|
|
12
|
+
connected: boolean;
|
|
13
|
+
statusText: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function getStore(): Store<AppState>;
|
|
16
|
+
export declare function useAppState(): AppState;
|
|
17
|
+
export declare function useSetState(): (updater: (prev: AppState) => AppState) => void;
|
|
18
|
+
export declare function updateAppState(updater: (prev: AppState) => AppState): void;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application state management — React context wrapping the generic store.
|
|
3
|
+
*/
|
|
4
|
+
import { useEffect, useState, useCallback } from 'react';
|
|
5
|
+
import { createStore } from '../store.js';
|
|
6
|
+
const initialState = {
|
|
7
|
+
messages: [],
|
|
8
|
+
currentMessage: null,
|
|
9
|
+
usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0, usageRatio: 0 },
|
|
10
|
+
planMode: false,
|
|
11
|
+
autoMode: false,
|
|
12
|
+
planWaiting: false,
|
|
13
|
+
scrollOffset: 0,
|
|
14
|
+
modelName: '',
|
|
15
|
+
connected: false,
|
|
16
|
+
statusText: 'connecting...',
|
|
17
|
+
};
|
|
18
|
+
let _store = null;
|
|
19
|
+
export function getStore() {
|
|
20
|
+
if (!_store)
|
|
21
|
+
_store = createStore(initialState);
|
|
22
|
+
return _store;
|
|
23
|
+
}
|
|
24
|
+
// Provide context manually since we need to use it outside React
|
|
25
|
+
// We use a module-level store accessed via getStore()
|
|
26
|
+
export function useAppState() {
|
|
27
|
+
const store = getStore();
|
|
28
|
+
const [state, setState] = useState(store.getState());
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
return store.subscribe(() => setState(store.getState()));
|
|
31
|
+
}, []);
|
|
32
|
+
return state;
|
|
33
|
+
}
|
|
34
|
+
export function useSetState() {
|
|
35
|
+
const store = getStore();
|
|
36
|
+
return useCallback((updater) => {
|
|
37
|
+
store.setState(updater);
|
|
38
|
+
}, []);
|
|
39
|
+
}
|
|
40
|
+
export function updateAppState(updater) {
|
|
41
|
+
getStore().setState(updater);
|
|
42
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jwcode/cli",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "JWCode — Java AI Coding Tool TypeScript CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/cli.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"jwcode": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"build": "node build.mjs",
|
|
14
|
+
"go": "node build.mjs && node dist/cli.js run",
|
|
15
|
+
"start": "node build.mjs && node dist/cli.js start",
|
|
16
|
+
"prepublishOnly": "node build.mjs --publish",
|
|
17
|
+
"postinstall": "node scripts/download-jre.js",
|
|
18
|
+
"build:jre": "node scripts/build-jre-zip.js"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"esbuild": "^0.28.0",
|
|
22
|
+
"ink": "^5.0.0",
|
|
23
|
+
"marked": "^14.1.0",
|
|
24
|
+
"react": "^18.3.1",
|
|
25
|
+
"ws": "^8.18.0"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist/",
|
|
29
|
+
"backend/"
|
|
30
|
+
],
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/react": "^18.3.12",
|
|
39
|
+
"@types/ws": "^8.5.13",
|
|
40
|
+
"typescript": "^5.6.3",
|
|
41
|
+
"vitest": "^4.1.7"
|
|
42
|
+
}
|
|
43
|
+
}
|