acmecode 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/.acmecode/config.json +6 -0
- package/README.md +124 -0
- package/dist/agent/index.js +161 -0
- package/dist/cli/bin/acmecode.js +3 -0
- package/dist/cli/package.json +25 -0
- package/dist/cli/src/index.d.ts +1 -0
- package/dist/cli/src/index.js +53 -0
- package/dist/config/index.js +92 -0
- package/dist/context/index.js +30 -0
- package/dist/core/src/agent/index.d.ts +52 -0
- package/dist/core/src/agent/index.js +476 -0
- package/dist/core/src/config/index.d.ts +83 -0
- package/dist/core/src/config/index.js +318 -0
- package/dist/core/src/context/index.d.ts +1 -0
- package/dist/core/src/context/index.js +30 -0
- package/dist/core/src/llm/provider.d.ts +27 -0
- package/dist/core/src/llm/provider.js +202 -0
- package/dist/core/src/llm/vision.d.ts +7 -0
- package/dist/core/src/llm/vision.js +37 -0
- package/dist/core/src/mcp/index.d.ts +10 -0
- package/dist/core/src/mcp/index.js +84 -0
- package/dist/core/src/prompt/anthropic.d.ts +1 -0
- package/dist/core/src/prompt/anthropic.js +32 -0
- package/dist/core/src/prompt/architect.d.ts +1 -0
- package/dist/core/src/prompt/architect.js +17 -0
- package/dist/core/src/prompt/autopilot.d.ts +1 -0
- package/dist/core/src/prompt/autopilot.js +18 -0
- package/dist/core/src/prompt/beast.d.ts +1 -0
- package/dist/core/src/prompt/beast.js +83 -0
- package/dist/core/src/prompt/gemini.d.ts +1 -0
- package/dist/core/src/prompt/gemini.js +45 -0
- package/dist/core/src/prompt/index.d.ts +18 -0
- package/dist/core/src/prompt/index.js +239 -0
- package/dist/core/src/prompt/zen.d.ts +1 -0
- package/dist/core/src/prompt/zen.js +13 -0
- package/dist/core/src/session/index.d.ts +18 -0
- package/dist/core/src/session/index.js +97 -0
- package/dist/core/src/skills/index.d.ts +6 -0
- package/dist/core/src/skills/index.js +72 -0
- package/dist/core/src/tools/batch.d.ts +2 -0
- package/dist/core/src/tools/batch.js +65 -0
- package/dist/core/src/tools/browser.d.ts +7 -0
- package/dist/core/src/tools/browser.js +86 -0
- package/dist/core/src/tools/edit.d.ts +11 -0
- package/dist/core/src/tools/edit.js +312 -0
- package/dist/core/src/tools/index.d.ts +13 -0
- package/dist/core/src/tools/index.js +980 -0
- package/dist/core/src/tools/lsp-client.d.ts +11 -0
- package/dist/core/src/tools/lsp-client.js +224 -0
- package/dist/index.js +41 -0
- package/dist/llm/provider.js +34 -0
- package/dist/mcp/index.js +84 -0
- package/dist/session/index.js +74 -0
- package/dist/skills/index.js +32 -0
- package/dist/tools/index.js +96 -0
- package/dist/tui/App.js +297 -0
- package/dist/tui/Spinner.js +16 -0
- package/dist/tui/TextInput.js +98 -0
- package/dist/tui/src/App.d.ts +11 -0
- package/dist/tui/src/App.js +1211 -0
- package/dist/tui/src/CatLogo.d.ts +10 -0
- package/dist/tui/src/CatLogo.js +99 -0
- package/dist/tui/src/OptionList.d.ts +15 -0
- package/dist/tui/src/OptionList.js +60 -0
- package/dist/tui/src/Spinner.d.ts +7 -0
- package/dist/tui/src/Spinner.js +18 -0
- package/dist/tui/src/TextInput.d.ts +28 -0
- package/dist/tui/src/TextInput.js +139 -0
- package/dist/tui/src/Tips.d.ts +2 -0
- package/dist/tui/src/Tips.js +62 -0
- package/dist/tui/src/Toast.d.ts +19 -0
- package/dist/tui/src/Toast.js +39 -0
- package/dist/tui/src/TodoItem.d.ts +7 -0
- package/dist/tui/src/TodoItem.js +21 -0
- package/dist/tui/src/i18n.d.ts +172 -0
- package/dist/tui/src/i18n.js +189 -0
- package/dist/tui/src/markdown.d.ts +6 -0
- package/dist/tui/src/markdown.js +356 -0
- package/dist/tui/src/theme.d.ts +31 -0
- package/dist/tui/src/theme.js +239 -0
- package/output.txt +0 -0
- package/package.json +44 -0
- package/packages/cli/package.json +25 -0
- package/packages/cli/src/index.ts +59 -0
- package/packages/cli/tsconfig.json +26 -0
- package/packages/core/package.json +39 -0
- package/packages/core/src/agent/index.ts +588 -0
- package/packages/core/src/config/index.ts +383 -0
- package/packages/core/src/context/index.ts +34 -0
- package/packages/core/src/llm/provider.ts +237 -0
- package/packages/core/src/llm/vision.ts +43 -0
- package/packages/core/src/mcp/index.ts +110 -0
- package/packages/core/src/prompt/anthropic.ts +32 -0
- package/packages/core/src/prompt/architect.ts +17 -0
- package/packages/core/src/prompt/autopilot.ts +18 -0
- package/packages/core/src/prompt/beast.ts +83 -0
- package/packages/core/src/prompt/gemini.ts +45 -0
- package/packages/core/src/prompt/index.ts +267 -0
- package/packages/core/src/prompt/zen.ts +13 -0
- package/packages/core/src/session/index.ts +129 -0
- package/packages/core/src/skills/index.ts +86 -0
- package/packages/core/src/tools/batch.ts +73 -0
- package/packages/core/src/tools/browser.ts +95 -0
- package/packages/core/src/tools/edit.ts +317 -0
- package/packages/core/src/tools/index.ts +1112 -0
- package/packages/core/src/tools/lsp-client.ts +303 -0
- package/packages/core/tsconfig.json +19 -0
- package/packages/tui/package.json +24 -0
- package/packages/tui/src/App.tsx +1702 -0
- package/packages/tui/src/CatLogo.tsx +134 -0
- package/packages/tui/src/OptionList.tsx +95 -0
- package/packages/tui/src/Spinner.tsx +28 -0
- package/packages/tui/src/TextInput.tsx +202 -0
- package/packages/tui/src/Tips.tsx +64 -0
- package/packages/tui/src/Toast.tsx +60 -0
- package/packages/tui/src/TodoItem.tsx +29 -0
- package/packages/tui/src/i18n.ts +203 -0
- package/packages/tui/src/markdown.ts +403 -0
- package/packages/tui/src/theme.ts +287 -0
- package/packages/tui/tsconfig.json +24 -0
- package/tsconfig.json +18 -0
- package/vscode-acmecode/.vscodeignore +11 -0
- package/vscode-acmecode/README.md +57 -0
- package/vscode-acmecode/esbuild.js +46 -0
- package/vscode-acmecode/images/button-dark.svg +5 -0
- package/vscode-acmecode/images/button-light.svg +5 -0
- package/vscode-acmecode/images/icon.png +1 -0
- package/vscode-acmecode/package-lock.json +490 -0
- package/vscode-acmecode/package.json +87 -0
- package/vscode-acmecode/src/extension.ts +128 -0
- package/vscode-acmecode/tsconfig.json +16 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface CatLogoProps {
|
|
3
|
+
provider?: string;
|
|
4
|
+
modelName?: string;
|
|
5
|
+
visionProvider?: string;
|
|
6
|
+
visionModel?: string;
|
|
7
|
+
version?: string;
|
|
8
|
+
}
|
|
9
|
+
export default function CatLogo({ provider, modelName, visionProvider, visionModel, version }: CatLogoProps): React.ReactElement;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { t } from './i18n.js';
|
|
4
|
+
import { theme } from './theme.js';
|
|
5
|
+
// ── Animated Cat Logo (Horizontal Layout) ──
|
|
6
|
+
// A compact card with the cat on the left and info on the right
|
|
7
|
+
const CAT_FRAMES = [
|
|
8
|
+
// Frame 0: Eyes open
|
|
9
|
+
[
|
|
10
|
+
` /\\_/\\ `,
|
|
11
|
+
` ( o.o ) `,
|
|
12
|
+
` > ^ < `,
|
|
13
|
+
` /| |\\ `,
|
|
14
|
+
`(_| |_)~`,
|
|
15
|
+
],
|
|
16
|
+
// Frame 1: Eyes open
|
|
17
|
+
[
|
|
18
|
+
` /\\_/\\ `,
|
|
19
|
+
` ( o.o ) `,
|
|
20
|
+
` > ^ < `,
|
|
21
|
+
` /| |\\ `,
|
|
22
|
+
`(_| |_)~`,
|
|
23
|
+
],
|
|
24
|
+
// Frame 2: Half blink
|
|
25
|
+
[
|
|
26
|
+
` /\\_/\\ `,
|
|
27
|
+
` ( -.o ) `,
|
|
28
|
+
` > ^ < `,
|
|
29
|
+
` /| |\\ `,
|
|
30
|
+
`(_| |_)~`,
|
|
31
|
+
],
|
|
32
|
+
// Frame 3: Full blink
|
|
33
|
+
[
|
|
34
|
+
` /\\_/\\ `,
|
|
35
|
+
` ( -.- ) `,
|
|
36
|
+
` > ^ < `,
|
|
37
|
+
` /| |\\ `,
|
|
38
|
+
`(_| |_)~`,
|
|
39
|
+
],
|
|
40
|
+
// Frame 4: Eyes open wide (surprise)
|
|
41
|
+
[
|
|
42
|
+
` /\\_/\\ `,
|
|
43
|
+
` ( O.O ) `,
|
|
44
|
+
` > ^ < `,
|
|
45
|
+
` /| |\\ `,
|
|
46
|
+
`(_| |_)~`,
|
|
47
|
+
],
|
|
48
|
+
// Frame 5: Eyes open
|
|
49
|
+
[
|
|
50
|
+
` /\\_/\\ `,
|
|
51
|
+
` ( o.o ) `,
|
|
52
|
+
` > ^ < `,
|
|
53
|
+
` /| |\\ `,
|
|
54
|
+
`(_| |_)~`,
|
|
55
|
+
],
|
|
56
|
+
];
|
|
57
|
+
// Animation sequence: mostly open eyes with occasional blinks
|
|
58
|
+
const SEQUENCE = [0, 1, 0, 5, 0, 1, 2, 3, 3, 2, 0, 1, 4, 4, 0, 5];
|
|
59
|
+
// Color palette for the gradient title
|
|
60
|
+
const TITLE_COLORS = [
|
|
61
|
+
'cyan',
|
|
62
|
+
'blue',
|
|
63
|
+
'magenta',
|
|
64
|
+
'blue',
|
|
65
|
+
];
|
|
66
|
+
export default function CatLogo({ provider, modelName, visionProvider, visionModel, version = 'v1.0' }) {
|
|
67
|
+
const [frame, setFrame] = useState(0);
|
|
68
|
+
const [titleColorIdx, setTitleColorIdx] = useState(0);
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const timer = setInterval(() => {
|
|
71
|
+
setFrame(prev => (prev + 1) % SEQUENCE.length);
|
|
72
|
+
setTitleColorIdx(prev => (prev + 1) % TITLE_COLORS.length);
|
|
73
|
+
}, 600);
|
|
74
|
+
return () => clearInterval(timer);
|
|
75
|
+
}, []);
|
|
76
|
+
const catLines = CAT_FRAMES[SEQUENCE[frame]];
|
|
77
|
+
const catColor = '#ffaf87';
|
|
78
|
+
const mutedColor = 'gray';
|
|
79
|
+
const vModel = visionProvider && visionModel ? `${visionProvider}:${visionModel}` : t('ui.disabled');
|
|
80
|
+
return (React.createElement(Box, { flexDirection: "row", paddingX: 1, paddingY: 0, borderStyle: "round", borderColor: theme.accentIdle.startsWith('#') ? undefined : theme.accentIdle, width: 65 },
|
|
81
|
+
React.createElement(Box, { flexDirection: "column", marginRight: 2, width: 10 }, catLines.map((line, i) => (React.createElement(Text, { key: i, color: catColor }, line)))),
|
|
82
|
+
React.createElement(Box, { flexDirection: "column", justifyContent: "center" },
|
|
83
|
+
React.createElement(Text, null,
|
|
84
|
+
React.createElement(Text, { bold: true, color: TITLE_COLORS[titleColorIdx] }, "AcmeCode"),
|
|
85
|
+
React.createElement(Text, { color: mutedColor },
|
|
86
|
+
" ",
|
|
87
|
+
version,
|
|
88
|
+
" | ",
|
|
89
|
+
t('ui.tagline'))),
|
|
90
|
+
React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
91
|
+
React.createElement(Text, { wrap: "truncate-end" },
|
|
92
|
+
React.createElement(Text, null, theme.primary('Model: ')),
|
|
93
|
+
React.createElement(Text, null, theme.primary(provider || '---')),
|
|
94
|
+
React.createElement(Text, null, theme.muted(':')),
|
|
95
|
+
React.createElement(Text, null, theme.info(modelName || '---'))),
|
|
96
|
+
React.createElement(Text, { wrap: "truncate-end" },
|
|
97
|
+
React.createElement(Text, null, theme.muted('Vision: ')),
|
|
98
|
+
React.createElement(Text, null, theme.info(vModel)))))));
|
|
99
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface Option {
|
|
3
|
+
id: string;
|
|
4
|
+
label: string;
|
|
5
|
+
}
|
|
6
|
+
interface OptionListProps {
|
|
7
|
+
options: Option[];
|
|
8
|
+
onSelect: (option: Option) => void;
|
|
9
|
+
onCancel: () => void;
|
|
10
|
+
title?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
currentId?: string;
|
|
13
|
+
}
|
|
14
|
+
export default function OptionList({ options, onSelect, onCancel, title, description, currentId }: OptionListProps): React.JSX.Element;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { theme } from './theme.js';
|
|
4
|
+
import { t } from './i18n.js';
|
|
5
|
+
export default function OptionList({ options, onSelect, onCancel, title, description, currentId }) {
|
|
6
|
+
const [selectedIndex, setSelectedIndex] = useState(() => {
|
|
7
|
+
const idx = options.findIndex(o => o.id === currentId);
|
|
8
|
+
return idx >= 0 ? idx : 0;
|
|
9
|
+
});
|
|
10
|
+
useInput((input, key) => {
|
|
11
|
+
if (key.upArrow) {
|
|
12
|
+
setSelectedIndex(prev => (prev > 0 ? prev - 1 : options.length - 1));
|
|
13
|
+
}
|
|
14
|
+
else if (key.downArrow) {
|
|
15
|
+
setSelectedIndex(prev => (prev < options.length - 1 ? prev + 1 : 0));
|
|
16
|
+
}
|
|
17
|
+
else if (key.return) {
|
|
18
|
+
onSelect(options[selectedIndex]);
|
|
19
|
+
}
|
|
20
|
+
else if (key.escape || (input === 'c' && key.ctrl)) {
|
|
21
|
+
onCancel();
|
|
22
|
+
}
|
|
23
|
+
else if (/^\d+$/.test(input)) {
|
|
24
|
+
const num = parseInt(input, 10);
|
|
25
|
+
if (num >= 1 && num <= options.length) {
|
|
26
|
+
onSelect(options[num - 1]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return (React.createElement(Box, { flexDirection: "column", paddingX: 1, marginY: 1 },
|
|
31
|
+
title && (React.createElement(Box, { marginBottom: 1 },
|
|
32
|
+
React.createElement(Text, { bold: true, underline: true }, title))),
|
|
33
|
+
description && (React.createElement(Box, { marginBottom: 1 },
|
|
34
|
+
React.createElement(Text, { dimColor: true }, description))),
|
|
35
|
+
React.createElement(Box, { flexDirection: "column" }, options.map((option, index) => {
|
|
36
|
+
const isSelected = index === selectedIndex;
|
|
37
|
+
const isCurrent = option.id === currentId;
|
|
38
|
+
return (React.createElement(Box, { key: option.id, flexDirection: "row" },
|
|
39
|
+
React.createElement(Box, { width: 3 },
|
|
40
|
+
React.createElement(Text, null, isSelected ? theme.primary('❯ ') : ' ')),
|
|
41
|
+
React.createElement(Box, { width: 3 },
|
|
42
|
+
React.createElement(Text, null, theme.muted(`${index + 1}.`))),
|
|
43
|
+
React.createElement(Box, { marginRight: 1 },
|
|
44
|
+
React.createElement(Text, null, isCurrent ? theme.success('●') : ' ')),
|
|
45
|
+
React.createElement(Text, { bold: isSelected }, isSelected ? theme.primary(option.label) : isCurrent ? theme.success(option.label) : option.label),
|
|
46
|
+
isCurrent && !isSelected && (React.createElement(Box, { marginLeft: 1 },
|
|
47
|
+
React.createElement(Text, null, theme.muted(`(${t('status.current')})`))))));
|
|
48
|
+
})),
|
|
49
|
+
React.createElement(Box, { marginTop: 1 },
|
|
50
|
+
React.createElement(Text, { dimColor: true },
|
|
51
|
+
"(Use ",
|
|
52
|
+
theme.primary('↑↓'),
|
|
53
|
+
" or ",
|
|
54
|
+
theme.primary('1-' + options.length),
|
|
55
|
+
" to select, ",
|
|
56
|
+
theme.primary('Enter'),
|
|
57
|
+
" to confirm, ",
|
|
58
|
+
theme.primary('Esc'),
|
|
59
|
+
" to cancel)"))));
|
|
60
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
4
|
+
const INTERVAL = 80;
|
|
5
|
+
export default function Spinner({ label, color = 'cyan' }) {
|
|
6
|
+
const [frame, setFrame] = useState(0);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const timer = setInterval(() => {
|
|
9
|
+
setFrame(prev => (prev + 1) % frames.length);
|
|
10
|
+
}, INTERVAL);
|
|
11
|
+
return () => clearInterval(timer);
|
|
12
|
+
}, []);
|
|
13
|
+
return (React.createElement(Text, null,
|
|
14
|
+
React.createElement(Text, { color: color },
|
|
15
|
+
frames[frame],
|
|
16
|
+
" "),
|
|
17
|
+
label ? React.createElement(Text, { color: "gray" }, label) : null));
|
|
18
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export interface PastedPart {
|
|
3
|
+
/** Placeholder shown in the input, e.g. "[Pasted ~12 lines]" */
|
|
4
|
+
label: string;
|
|
5
|
+
/** Full original text to be expanded on submit */
|
|
6
|
+
text: string;
|
|
7
|
+
}
|
|
8
|
+
interface TextInputProps {
|
|
9
|
+
value: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
focus?: boolean;
|
|
12
|
+
showCursor?: boolean;
|
|
13
|
+
onChange: (value: string) => void;
|
|
14
|
+
onSubmit?: (value: string) => void;
|
|
15
|
+
onUp?: () => void;
|
|
16
|
+
onDown?: () => void;
|
|
17
|
+
onTab?: () => void;
|
|
18
|
+
/** Called when a large paste is detected and should be collapsed */
|
|
19
|
+
onPaste?: (part: PastedPart) => void;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Custom TextInput that fixes the stale-closure bug in ink-text-input@6.0.0.
|
|
23
|
+
* Also detects large pastes (≥3 lines or >150 chars) and calls onPaste so the
|
|
24
|
+
* caller can collapse them into a "[Pasted ~N lines]" token — matching opencode.
|
|
25
|
+
*/
|
|
26
|
+
declare function TextInput({ value, placeholder, focus, showCursor, onChange, onSubmit, onUp, onDown, onTab, onPaste, }: TextInputProps): React.JSX.Element;
|
|
27
|
+
declare const _default: React.MemoExoticComponent<typeof TextInput>;
|
|
28
|
+
export default _default;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import React, { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { Text, useInput } from "ink";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { theme } from "./theme.js";
|
|
5
|
+
// Threshold matching opencode: ≥3 lines or >150 chars triggers paste collapse
|
|
6
|
+
const PASTE_LINE_THRESHOLD = 3;
|
|
7
|
+
const PASTE_CHAR_THRESHOLD = 150;
|
|
8
|
+
/**
|
|
9
|
+
* Custom TextInput that fixes the stale-closure bug in ink-text-input@6.0.0.
|
|
10
|
+
* Also detects large pastes (≥3 lines or >150 chars) and calls onPaste so the
|
|
11
|
+
* caller can collapse them into a "[Pasted ~N lines]" token — matching opencode.
|
|
12
|
+
*/
|
|
13
|
+
function TextInput({ value, placeholder = "", focus = true, showCursor = true, onChange, onSubmit, onUp, onDown, onTab, onPaste, }) {
|
|
14
|
+
const [cursorOffset, setCursorOffset] = useState(value.length);
|
|
15
|
+
const valueRef = useRef(value);
|
|
16
|
+
const cursorRef = useRef(cursorOffset);
|
|
17
|
+
const onChangeRef = useRef(onChange);
|
|
18
|
+
const onSubmitRef = useRef(onSubmit);
|
|
19
|
+
const onUpRef = useRef(onUp);
|
|
20
|
+
const onDownRef = useRef(onDown);
|
|
21
|
+
const onTabRef = useRef(onTab);
|
|
22
|
+
const onPasteRef = useRef(onPaste);
|
|
23
|
+
valueRef.current = value;
|
|
24
|
+
cursorRef.current = cursorOffset;
|
|
25
|
+
onChangeRef.current = onChange;
|
|
26
|
+
onSubmitRef.current = onSubmit;
|
|
27
|
+
onUpRef.current = onUp;
|
|
28
|
+
onDownRef.current = onDown;
|
|
29
|
+
onTabRef.current = onTab;
|
|
30
|
+
onPasteRef.current = onPaste;
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (cursorOffset > value.length) {
|
|
33
|
+
setCursorOffset(value.length);
|
|
34
|
+
}
|
|
35
|
+
}, [value, cursorOffset]);
|
|
36
|
+
useInput((input, key) => {
|
|
37
|
+
if (key.upArrow) {
|
|
38
|
+
onUpRef.current?.();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (key.downArrow) {
|
|
42
|
+
onDownRef.current?.();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (key.tab || (key.shift && key.tab)) {
|
|
46
|
+
onTabRef.current?.();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if ((key.ctrl && input === "c") || key.escape)
|
|
50
|
+
return;
|
|
51
|
+
const currentValue = valueRef.current;
|
|
52
|
+
const currentCursor = cursorRef.current;
|
|
53
|
+
if (key.return) {
|
|
54
|
+
onSubmitRef.current?.(currentValue);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (key.leftArrow) {
|
|
58
|
+
const next = Math.max(0, currentCursor - 1);
|
|
59
|
+
setCursorOffset(next);
|
|
60
|
+
cursorRef.current = next;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (key.rightArrow) {
|
|
64
|
+
const next = Math.min(currentValue.length, currentCursor + 1);
|
|
65
|
+
setCursorOffset(next);
|
|
66
|
+
cursorRef.current = next;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (key.backspace || key.delete) {
|
|
70
|
+
if (currentCursor > 0) {
|
|
71
|
+
const nextValue = currentValue.slice(0, currentCursor - 1) +
|
|
72
|
+
currentValue.slice(currentCursor);
|
|
73
|
+
const nextCursor = currentCursor - 1;
|
|
74
|
+
setCursorOffset(nextCursor);
|
|
75
|
+
cursorRef.current = nextCursor;
|
|
76
|
+
valueRef.current = nextValue;
|
|
77
|
+
onChangeRef.current(nextValue);
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// ── Paste detection ──
|
|
82
|
+
// Terminals send pasted text as a burst of characters in a single input event.
|
|
83
|
+
// Detect by: multi-char input that isn't a known control sequence.
|
|
84
|
+
if (input.length > 1 && onPasteRef.current) {
|
|
85
|
+
const normalized = input.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
86
|
+
const lineCount = (normalized.match(/\n/g)?.length ?? 0) + 1;
|
|
87
|
+
if (lineCount >= PASTE_LINE_THRESHOLD ||
|
|
88
|
+
normalized.length > PASTE_CHAR_THRESHOLD) {
|
|
89
|
+
const label = `[Pasted ~${lineCount} lines]`;
|
|
90
|
+
onPasteRef.current({ label, text: normalized });
|
|
91
|
+
// Insert the label token into the input at cursor position
|
|
92
|
+
const nextValue = currentValue.slice(0, currentCursor) +
|
|
93
|
+
label +
|
|
94
|
+
currentValue.slice(currentCursor);
|
|
95
|
+
const nextCursor = currentCursor + label.length;
|
|
96
|
+
setCursorOffset(nextCursor);
|
|
97
|
+
cursorRef.current = nextCursor;
|
|
98
|
+
valueRef.current = nextValue;
|
|
99
|
+
onChangeRef.current(nextValue);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Normal character input (including multi-char IME input)
|
|
104
|
+
const nextValue = currentValue.slice(0, currentCursor) +
|
|
105
|
+
input +
|
|
106
|
+
currentValue.slice(currentCursor);
|
|
107
|
+
const nextCursor = currentCursor + input.length;
|
|
108
|
+
setCursorOffset(nextCursor);
|
|
109
|
+
cursorRef.current = nextCursor;
|
|
110
|
+
valueRef.current = nextValue;
|
|
111
|
+
onChangeRef.current(nextValue);
|
|
112
|
+
}, { isActive: focus });
|
|
113
|
+
// ── Render ──
|
|
114
|
+
if (showCursor && focus) {
|
|
115
|
+
if (value.length === 0) {
|
|
116
|
+
if (placeholder) {
|
|
117
|
+
return (React.createElement(Text, null, chalk.inverse(placeholder[0]) + chalk.grey(placeholder.slice(1))));
|
|
118
|
+
}
|
|
119
|
+
return React.createElement(Text, null, chalk.inverse(" "));
|
|
120
|
+
}
|
|
121
|
+
const before = value.slice(0, cursorOffset);
|
|
122
|
+
const cursorChar = cursorOffset < value.length ? value[cursorOffset] : " ";
|
|
123
|
+
const after = cursorOffset < value.length ? value.slice(cursorOffset + 1) : "";
|
|
124
|
+
// Highlight [Pasted ~N lines] tokens in cyan
|
|
125
|
+
return (React.createElement(Text, null,
|
|
126
|
+
renderWithTokens(before),
|
|
127
|
+
chalk.inverse(cursorChar),
|
|
128
|
+
renderWithTokens(after)));
|
|
129
|
+
}
|
|
130
|
+
if (value.length === 0 && placeholder) {
|
|
131
|
+
return React.createElement(Text, null, chalk.grey(placeholder));
|
|
132
|
+
}
|
|
133
|
+
return React.createElement(Text, null, renderWithTokens(value));
|
|
134
|
+
}
|
|
135
|
+
/** Highlight [Pasted ~N lines] tokens so they stand out */
|
|
136
|
+
function renderWithTokens(text) {
|
|
137
|
+
return text.replace(/\[Pasted ~\d+ lines\]/g, (m) => theme.primary(m));
|
|
138
|
+
}
|
|
139
|
+
export default React.memo(TextInput);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { getLang, t } from './i18n.js';
|
|
4
|
+
import { theme } from './theme.js';
|
|
5
|
+
const TIPS_EN = [
|
|
6
|
+
`Type ${theme.primary('/model')} to switch AI model`,
|
|
7
|
+
`Type ${theme.primary('/skill')} to view and activate skills`,
|
|
8
|
+
`Type ${theme.primary('/clear')} to clear conversation history`,
|
|
9
|
+
`Type ${theme.primary('/cancel')} to abort current generation`,
|
|
10
|
+
`Type ${theme.primary('/theme')} to change UI theme`,
|
|
11
|
+
`Type ${theme.primary('/lang')} to change UI language`,
|
|
12
|
+
`Create ${theme.primary('.acmecode.md')} in root to define AI rules`,
|
|
13
|
+
`AI segments large file reads to avoid context overflow`,
|
|
14
|
+
`AI auto-fixes tool names if it makes a typo`,
|
|
15
|
+
`${theme.primary('Ctrl+C')} aborts current operation`,
|
|
16
|
+
`Supports OpenAI, Claude, Gemini, Grok and more`,
|
|
17
|
+
`Use ${theme.primary('acmecode --list')} to see sessions`,
|
|
18
|
+
`Use ${theme.primary('acmecode [dir]')} to start in a directory`,
|
|
19
|
+
`Environment context (CWD, OS, Git) is automatically injected`,
|
|
20
|
+
`Specific prompts are optimized per AI family (Beast/Anthropic/Gemini)`,
|
|
21
|
+
`${theme.primary('AGENTS.md')} is automatically loaded as project spec`,
|
|
22
|
+
`Markdown rendering supports headers, code, lists, and checkboxes`,
|
|
23
|
+
`Use ${theme.primary('/model provider:model')} for quick switch`,
|
|
24
|
+
`Tool results are truncated if super long to save tokens`,
|
|
25
|
+
`Session titles are auto-generated from your first message`,
|
|
26
|
+
];
|
|
27
|
+
const TIPS_ZH = [
|
|
28
|
+
`输入 ${theme.primary('/model')} 切换 AI 模型`,
|
|
29
|
+
`输入 ${theme.primary('/skill')} 查看和激活技能`,
|
|
30
|
+
`输入 ${theme.primary('/clear')} 清空对话历史`,
|
|
31
|
+
`输入 ${theme.primary('/cancel')} 中断正在生成的回复`,
|
|
32
|
+
`输入 ${theme.primary('/theme')} 切换界面主题`,
|
|
33
|
+
`输入 ${theme.primary('/lang')} 切换界面语言`,
|
|
34
|
+
`在项目根目录创建 ${theme.primary('.acmecode.md')} 文件来定义 AI 编码规范`,
|
|
35
|
+
`读大文件时,AI 会自动分段读取,避免上下文溢出`,
|
|
36
|
+
`工具调用失败时,AI 会自动修复工具名称并重试`,
|
|
37
|
+
`${theme.primary('Ctrl+C')} 可以中断当前操作`,
|
|
38
|
+
`支持 OpenAI、Claude、Gemini、Grok 等 8 种 AI 模型`,
|
|
39
|
+
`使用 ${theme.primary('acmecode --list')} 查看所有历史会话`,
|
|
40
|
+
`使用 ${theme.primary('acmecode [dir]')} 在指定目录启动 AcmeCode`,
|
|
41
|
+
`系统会自动注入工作目录、操作系统等环境上下文给 AI`,
|
|
42
|
+
`模型特定提示词会根据你使用的 AI 自动优化`,
|
|
43
|
+
`${theme.primary('AGENTS.md')} 和 ${theme.primary('CLAUDE.md')} 文件也会被自动加载为项目规范`,
|
|
44
|
+
`AI 回复支持 Markdown 渲染:标题、代码块、列表都会美化显示`,
|
|
45
|
+
`使用 ${theme.primary('/model provider:model')} 快速切换到指定模型`,
|
|
46
|
+
`每个 AI 模型有独立优化的系统提示词(Beast/Anthropic/Gemini 三大家族)`,
|
|
47
|
+
`工具结果超长时会自动截断,但完整内容仍会发送给 AI`,
|
|
48
|
+
`会话标题会自动从你的第一条消息生成`,
|
|
49
|
+
];
|
|
50
|
+
export default function Tips() {
|
|
51
|
+
const [tip] = useState(() => {
|
|
52
|
+
const list = getLang() === 'zh' ? TIPS_ZH : TIPS_EN;
|
|
53
|
+
return list[Math.floor(Math.random() * list.length)];
|
|
54
|
+
});
|
|
55
|
+
return (React.createElement(Box, null,
|
|
56
|
+
React.createElement(Text, null,
|
|
57
|
+
theme.warning('● ' + t('tip.prefix')),
|
|
58
|
+
" ",
|
|
59
|
+
theme.muted('─'),
|
|
60
|
+
" ",
|
|
61
|
+
tip)));
|
|
62
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export type ToastVariant = 'success' | 'error' | 'warning' | 'info';
|
|
3
|
+
interface ToastData {
|
|
4
|
+
message: string;
|
|
5
|
+
variant: ToastVariant;
|
|
6
|
+
title?: string;
|
|
7
|
+
}
|
|
8
|
+
interface ToastProps {
|
|
9
|
+
toast: ToastData | null;
|
|
10
|
+
onDismiss: () => void;
|
|
11
|
+
duration?: number;
|
|
12
|
+
}
|
|
13
|
+
export default function Toast({ toast, onDismiss, duration }: ToastProps): React.ReactElement | null;
|
|
14
|
+
export declare function useToast(): {
|
|
15
|
+
toast: ToastData | null;
|
|
16
|
+
show: (variant: ToastVariant, message: string, title?: string) => void;
|
|
17
|
+
dismiss: () => void;
|
|
18
|
+
};
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
const VARIANT_STYLES = {
|
|
5
|
+
success: { icon: '✔', color: pc.green },
|
|
6
|
+
error: { icon: '✖', color: pc.red },
|
|
7
|
+
warning: { icon: '⚠', color: pc.yellow },
|
|
8
|
+
info: { icon: 'ℹ', color: pc.cyan },
|
|
9
|
+
};
|
|
10
|
+
export default function Toast({ toast, onDismiss, duration = 3000 }) {
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
if (!toast)
|
|
13
|
+
return;
|
|
14
|
+
const timer = setTimeout(onDismiss, duration);
|
|
15
|
+
return () => clearTimeout(timer);
|
|
16
|
+
}, [toast, duration, onDismiss]);
|
|
17
|
+
if (!toast)
|
|
18
|
+
return null;
|
|
19
|
+
const style = VARIANT_STYLES[toast.variant];
|
|
20
|
+
const border = style.color('┃');
|
|
21
|
+
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
22
|
+
React.createElement(Box, null,
|
|
23
|
+
React.createElement(Text, null,
|
|
24
|
+
border,
|
|
25
|
+
" ",
|
|
26
|
+
style.color(style.icon),
|
|
27
|
+
" ",
|
|
28
|
+
toast.title ? pc.bold(toast.title) + ' — ' : '',
|
|
29
|
+
toast.message))));
|
|
30
|
+
}
|
|
31
|
+
// ── Toast hook for easy usage ──
|
|
32
|
+
export function useToast() {
|
|
33
|
+
const [toast, setToast] = useState(null);
|
|
34
|
+
const show = (variant, message, title) => {
|
|
35
|
+
setToast({ message, variant, title });
|
|
36
|
+
};
|
|
37
|
+
const dismiss = () => setToast(null);
|
|
38
|
+
return { toast, show, dismiss };
|
|
39
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
const STATUS_ICONS = {
|
|
5
|
+
pending: pc.dim('○'),
|
|
6
|
+
in_progress: pc.yellow('●'),
|
|
7
|
+
completed: pc.green('✔'),
|
|
8
|
+
};
|
|
9
|
+
export default function TodoItem({ status, content }) {
|
|
10
|
+
const icon = STATUS_ICONS[status] || pc.dim('○');
|
|
11
|
+
const text = status === 'completed'
|
|
12
|
+
? pc.strikethrough(pc.dim(content))
|
|
13
|
+
: status === 'in_progress'
|
|
14
|
+
? pc.yellow(content)
|
|
15
|
+
: content;
|
|
16
|
+
return (React.createElement(Box, null,
|
|
17
|
+
React.createElement(Text, null,
|
|
18
|
+
icon,
|
|
19
|
+
" ",
|
|
20
|
+
text)));
|
|
21
|
+
}
|