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