termux-dev 1.4.19 → 1.6.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 +15 -3
- package/assets/banner.svg +1 -1
- package/assets/preview.png +0 -0
- package/assets/preview.svg +12 -0
- package/bin/devx.js +0 -0
- package/dist/cli/banner.js +45 -0
- package/dist/cli/diff.js +64 -0
- package/dist/cli/doctor.js +2 -1
- package/dist/cli/headless.js +14 -8
- package/dist/cli/history_search.js +111 -0
- package/dist/cli/index.js +137 -133
- package/dist/cli/modeborder.js +31 -0
- package/dist/cli/prompt.js +138 -92
- package/dist/cli/prompt_palette.js +43 -0
- package/dist/cli/server.js +2 -1
- package/dist/cli/statusline.js +133 -0
- package/dist/cli/telemetry.js +105 -0
- package/dist/core/types.js +1 -1
- package/dist/mcp/client.js +2 -1
- package/dist/permissions/guard.js +9 -4
- package/dist/providers/openai.js +5 -4
- package/dist/tools/fs.js +41 -7
- package/dist/tools/packages.js +35 -11
- package/dist/tools/search.js +24 -5
- package/package.json +8 -3
- package/assets/preview_work1.png +0 -0
- package/dist/cli/index (# Edit conflict 2026-08-29 pe1jzcC #).js +0 -1966
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Built specifically for **Android Termux**, **Windows**, **macOS**, and **Linux**
|
|
|
20
20
|
|
|
21
21
|
<br/><br/>
|
|
22
22
|
|
|
23
|
-
<img src="assets/preview.
|
|
23
|
+
<img src="assets/preview.svg" alt="devx Terminal Interface" width="750" style="border-radius: 8px;" />
|
|
24
24
|
|
|
25
25
|
</div>
|
|
26
26
|
|
|
@@ -28,6 +28,8 @@ Built specifically for **Android Termux**, **Windows**, **macOS**, and **Linux**
|
|
|
28
28
|
|
|
29
29
|
## 🌟 Highlights
|
|
30
30
|
|
|
31
|
+
The terminal interface uses a compact layout on both desktop and Termux. Switch to the original ASCII layout in `/settings` → `Interface`.
|
|
32
|
+
|
|
31
33
|
- **🧠 Dual-Brain Architecture (PLAN & AGENT Modes):**
|
|
32
34
|
Instantly toggle between **PLAN** (safe architect, interactive requirements questionnaire, no code dumps) and **AGENT** (autonomous file edits, terminal commands, auto-installer).
|
|
33
35
|
- **🚀 One-Click Plan Approval (`[🚀 Go / ✏️ Other]`):**
|
|
@@ -46,6 +48,10 @@ Built specifically for **Android Termux**, **Windows**, **macOS**, and **Linux**
|
|
|
46
48
|
Deep OLED black background designed for distraction-free CLI vibe-coding.
|
|
47
49
|
- **📜 Smart Session History (`/resume` & `/session`):**
|
|
48
50
|
Resume sessions with last 20 messages neatly rendered in full Markdown and track unique session IDs (`#a8f9z`).
|
|
51
|
+
- **🔍 Smart Multi-Line Editor (`edit_file 2.0`):**
|
|
52
|
+
Collision-safe code editing with line-number error reporting on duplicate matches and `replaceAll` support.
|
|
53
|
+
- **🧪 Comprehensive Test Suite (Vitest):**
|
|
54
|
+
Full test coverage across core agent loop, file systems, security guard, snapshots, and context history with automated CI.
|
|
49
55
|
- **⚡ Universal AI Provider Support:**
|
|
50
56
|
**OpenRouter**, **Google Gemini**, **DeepSeek**, **Groq**, **Mistral**, **OpenAI**, **Anthropic**, **Alibaba**, and local models (**Ollama**, **LM Studio**).
|
|
51
57
|
|
|
@@ -151,7 +157,7 @@ Type `/` in the prompt to open the autocomplete command palette:
|
|
|
151
157
|
| **`/model`** | Switch model for the active provider with live search |
|
|
152
158
|
| **`/provider`** | Switch provider (OpenRouter, Gemini, Groq, DeepSeek, Local) |
|
|
153
159
|
| **`/compact`** | Compact and summarize chat context tokens |
|
|
154
|
-
| **`/clear`** | Clear screen
|
|
160
|
+
| **`/clear`** | Clear screen without deleting conversation history |
|
|
155
161
|
| **`/init`** | Generate `AGENTS.md` developer guide in project root |
|
|
156
162
|
| **`/help`** | Display all available commands |
|
|
157
163
|
| **`/exit`** | Exit devx cleanly |
|
|
@@ -178,6 +184,8 @@ If you find **devx** useful, please consider giving it a ⭐️ star on [GitHub]
|
|
|
178
184
|
|
|
179
185
|
# 🇷🇺 Русский
|
|
180
186
|
|
|
187
|
+
Компактный интерфейс используется по умолчанию и на ПК, и в Termux. Исходный ASCII-вариант можно включить в `/settings` → `Interface`.
|
|
188
|
+
|
|
181
189
|
## 🌟 Главные возможности
|
|
182
190
|
|
|
183
191
|
- **🧠 Двухрежимная архитектура (PLAN и AGENT):**
|
|
@@ -198,6 +206,10 @@ If you find **devx** useful, please consider giving it a ⭐️ star on [GitHub]
|
|
|
198
206
|
Глубокий OLED-чёрный фон в стиле OpenCode для комфортной ночной разработки.
|
|
199
207
|
- **📜 История сессий с уникальными ID (`/resume` и `/session`):**
|
|
200
208
|
Восстановление сессий с предпросмотром последних 20 сообщений в Markdown и короткими хэш-ID (`#a8f9z`).
|
|
209
|
+
- **🔍 Умный редактор `edit_file 2.0`:**
|
|
210
|
+
Безопасная замена фрагментов кода с защитой от коллизий при дубликатах, указанием номеров строк и поддержкой `replaceAll`.
|
|
211
|
+
- **🧪 Тестовый фундамент (Vitest):**
|
|
212
|
+
Полное покрытие юнит-тестами ядра агента, файловой системы, безопасности, снимков и истории контекста с авто-тестами в CI.
|
|
201
213
|
- **⚡ Поддержка любых провайдеров:**
|
|
202
214
|
**OpenRouter**, **Google Gemini**, **DeepSeek**, **Groq**, **Mistral**, **OpenAI**, **Anthropic**, **Alibaba**, а также локальные **Ollama** и **LM Studio**.
|
|
203
215
|
|
|
@@ -303,7 +315,7 @@ devx
|
|
|
303
315
|
| **`/model`** | Сменить модель текущего провайдера с живым поиском |
|
|
304
316
|
| **`/provider`** | Сменить AI-провайдера (OpenRouter, Gemini, Groq, DeepSeek, Local) |
|
|
305
317
|
| **`/compact`** | Сжать контекст диалога для экономии токенов |
|
|
306
|
-
| **`/clear`** | Очистить экран
|
|
318
|
+
| **`/clear`** | Очистить экран терминала, сохранив историю диалога |
|
|
307
319
|
| **`/init`** | Создать файл инструкций `AGENTS.md` в корне проекта |
|
|
308
320
|
| **`/help`** | Показать список всех доступных команд |
|
|
309
321
|
| **`/exit`** | Завершить работу devx |
|
package/assets/banner.svg
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
<!-- Tagline & Badge -->
|
|
27
27
|
<text x="425" y="142" font-family="system-ui, -apple-system, sans-serif" font-size="14" font-weight="600" fill="#8b949e" text-anchor="middle" letter-spacing="3">
|
|
28
|
-
THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.
|
|
28
|
+
THE TERMINAL-NATIVE AI CODING AGENT <tspan fill="#00f2fe" font-weight="bold">v1.6.0</tspan>
|
|
29
29
|
</text>
|
|
30
30
|
|
|
31
31
|
<!-- Top Accent Line -->
|
package/assets/preview.png
CHANGED
|
Binary file
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="760" height="220" viewBox="0 0 760 220">
|
|
2
|
+
<rect width="760" height="220" rx="12" fill="#0a0a0c"/>
|
|
3
|
+
<g font-family="DejaVu Sans Mono, Consolas, monospace" font-size="18">
|
|
4
|
+
<text x="32" y="52" fill="#f1f5f9" font-weight="700">devx</text>
|
|
5
|
+
<text x="92" y="52" fill="#84909c">v1.6.0</text>
|
|
6
|
+
<text x="32" y="102" fill="#67d8e2">agent</text>
|
|
7
|
+
<text x="104" y="102" fill="#e2e8f0" font-weight="700">minimax/minimax-m3:free</text>
|
|
8
|
+
<text x="32" y="132" fill="#84909c">context 0% · $0.0000 · main</text>
|
|
9
|
+
<text x="32" y="186" fill="#67d8e2">›</text>
|
|
10
|
+
<text x="56" y="186" fill="#84909c">Ask or describe a task…</text>
|
|
11
|
+
</g>
|
|
12
|
+
</svg>
|
package/bin/devx.js
CHANGED
|
File without changes
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { getCurrentTheme } from './theme.js';
|
|
3
|
+
import { DEVX_VERSION } from '../core/types.js';
|
|
4
|
+
export function renderBannerLines(mode = 'full', version = DEVX_VERSION, cols = 80) {
|
|
5
|
+
if (mode === 'off') {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
const theme = getCurrentTheme();
|
|
9
|
+
if (mode === 'clean') {
|
|
10
|
+
return [pc.bold('devx') + pc.dim(` v${version}`)];
|
|
11
|
+
}
|
|
12
|
+
if (mode === 'minimal') {
|
|
13
|
+
return [
|
|
14
|
+
theme.colorFn('⚡ ') + theme.boldFn('TERMUX·DEV') + ' ' + pc.dim(`v${version}`) + pc.dim(' · Type /help for commands')
|
|
15
|
+
];
|
|
16
|
+
}
|
|
17
|
+
// Full ASCII Banner
|
|
18
|
+
if (cols < 68) {
|
|
19
|
+
// Compact ASCII for narrow mobile screens (~26 chars)
|
|
20
|
+
return [
|
|
21
|
+
theme.colorFn(' █▀▀▄ █▀▀▀ █ █ █ █'),
|
|
22
|
+
theme.colorFn(' █ █ █▀▀▀ ▀▄▀ ▀▄▀ '),
|
|
23
|
+
theme.colorFn(' █▄▄▀ █▄▄▄ ▀ ▀ ▀ '),
|
|
24
|
+
' ' + theme.boldFn(`v${version}`)
|
|
25
|
+
];
|
|
26
|
+
}
|
|
27
|
+
// Full wide ASCII banner (53 chars)
|
|
28
|
+
const indent = ' ';
|
|
29
|
+
return [
|
|
30
|
+
indent + theme.colorFn('▀▀▀█▀▀▀ █▀▀▀ █▀▀█ █▄ ▄█ █ █ ▀▄ ▄▀ █▀▀▄ █▀▀▀ █ █'),
|
|
31
|
+
indent + theme.colorFn(' █ █▀▀▀ █▄▄▀ █ █ █ █ █ █ ▀▀ █ █ █▀▀▀ █ █'),
|
|
32
|
+
indent + theme.colorFn(' █ █▄▄▄ █ ▀▄ █ █ ▀▄▄▀ ▄▀ ▀▄ █▄▄▀ █▄▄▄ ▀▄▀ '),
|
|
33
|
+
indent + theme.boldFn(`v${version}`)
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
export function drawBanner(mode = 'full', version = DEVX_VERSION, cols = 80) {
|
|
37
|
+
const lines = renderBannerLines(mode, version, cols);
|
|
38
|
+
if (lines.length > 0) {
|
|
39
|
+
console.log();
|
|
40
|
+
for (const l of lines) {
|
|
41
|
+
console.log(l);
|
|
42
|
+
}
|
|
43
|
+
console.log();
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/cli/diff.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
import { getCurrentTheme } from './theme.js';
|
|
3
|
+
export function formatDiffBox(fileName, diffLines, cols = 80, summary, diffColors = false) {
|
|
4
|
+
const theme = getCurrentTheme();
|
|
5
|
+
const boxWidth = Math.max(30, Math.min(cols - 4, 76));
|
|
6
|
+
const cleanFileName = typeof fileName === 'string' ? fileName.split(/[\/\\]/).pop() || 'file' : 'file';
|
|
7
|
+
const fillCount = Math.max(2, boxWidth - 5 - cleanFileName.length);
|
|
8
|
+
const lines = [
|
|
9
|
+
theme.colorFn('┌─ ') + pc.bold(cleanFileName) + ' ' + theme.colorFn('─'.repeat(fillCount) + '┐')
|
|
10
|
+
];
|
|
11
|
+
const maxShown = Math.min(diffLines.length, 30);
|
|
12
|
+
for (let i = 0; i < maxShown; i++) {
|
|
13
|
+
const rawLine = diffLines[i];
|
|
14
|
+
const match = rawLine.match(/^(\d+)(\s+[+\- ]\s+)(.*)$/);
|
|
15
|
+
if (match) {
|
|
16
|
+
const lineNum = match[1].padStart(4, ' ');
|
|
17
|
+
const symbol = match[2];
|
|
18
|
+
const contentStr = match[3] || '';
|
|
19
|
+
const isAdd = symbol.includes('+');
|
|
20
|
+
const isRemove = symbol.includes('-');
|
|
21
|
+
const prefix = isRemove ? '-' : isAdd ? '+' : ' ';
|
|
22
|
+
const maxCodeLen = Math.max(10, boxWidth - 11);
|
|
23
|
+
const paddedCode = contentStr.length > maxCodeLen
|
|
24
|
+
? contentStr.substring(0, maxCodeLen - 1) + '…'
|
|
25
|
+
: contentStr.padEnd(maxCodeLen, ' ');
|
|
26
|
+
const innerRow = ` ${lineNum} ${prefix} ${paddedCode} `;
|
|
27
|
+
if (diffColors) {
|
|
28
|
+
if (isRemove) {
|
|
29
|
+
lines.push(theme.colorFn('│') + pc.red(innerRow) + theme.colorFn('│'));
|
|
30
|
+
}
|
|
31
|
+
else if (isAdd) {
|
|
32
|
+
lines.push(theme.colorFn('│') + pc.green(innerRow) + theme.colorFn('│'));
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
lines.push(theme.colorFn('│') + pc.dim(innerRow) + theme.colorFn('│'));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
if (isRemove || isAdd) {
|
|
40
|
+
lines.push(theme.colorFn('│') + pc.white(innerRow) + theme.colorFn('│'));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
lines.push(theme.colorFn('│') + pc.dim(innerRow) + theme.colorFn('│'));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const maxLen = Math.max(10, boxWidth - 4);
|
|
49
|
+
const padded = rawLine.length > maxLen ? rawLine.substring(0, maxLen - 1) + '…' : rawLine.padEnd(maxLen, ' ');
|
|
50
|
+
lines.push(theme.colorFn('│ ') + pc.white(padded) + theme.colorFn(' │'));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (diffLines.length > maxShown) {
|
|
54
|
+
const dots = `... +${diffLines.length - maxShown} more lines`;
|
|
55
|
+
const maxLen = Math.max(10, boxWidth - 4);
|
|
56
|
+
const paddedDots = dots.length > maxLen ? dots.substring(0, maxLen - 1) + '…' : dots.padEnd(maxLen, ' ');
|
|
57
|
+
lines.push(theme.colorFn('│ ') + pc.dim(paddedDots) + theme.colorFn(' │'));
|
|
58
|
+
}
|
|
59
|
+
lines.push(theme.colorFn('└' + '─'.repeat(boxWidth - 2) + '┘'));
|
|
60
|
+
if (summary) {
|
|
61
|
+
lines.push(theme.boldFn(` └─ ${summary}`));
|
|
62
|
+
}
|
|
63
|
+
return lines.join('\n');
|
|
64
|
+
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -2,6 +2,7 @@ import os from 'os';
|
|
|
2
2
|
import { execSync } from 'child_process';
|
|
3
3
|
import https from 'https';
|
|
4
4
|
import pc from 'picocolors';
|
|
5
|
+
import { DEVX_VERSION } from '../core/types.js';
|
|
5
6
|
import { getCurrentTheme } from './theme.js';
|
|
6
7
|
import { isTermux } from '../core/notify.js';
|
|
7
8
|
function checkCmd(cmd) {
|
|
@@ -18,7 +19,7 @@ async function pingUrl(url) {
|
|
|
18
19
|
const req = https.get(url, {
|
|
19
20
|
timeout: 3000,
|
|
20
21
|
headers: {
|
|
21
|
-
'User-Agent':
|
|
22
|
+
'User-Agent': `devx-doctor/${DEVX_VERSION}`
|
|
22
23
|
}
|
|
23
24
|
}, (res) => {
|
|
24
25
|
res.resume(); // consume response data to free up memory and release socket
|
package/dist/cli/headless.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import pc from 'picocolors';
|
|
2
|
+
import { DEVX_VERSION } from '../core/types.js';
|
|
2
3
|
import { History } from '../core/history.js';
|
|
3
4
|
import { Agent } from '../core/loop.js';
|
|
4
5
|
import { buildSystemPrompt } from '../prompts/builder.js';
|
|
@@ -7,6 +8,7 @@ import { getTools } from '../tools/index.js';
|
|
|
7
8
|
import { CLIConsoleGuard } from '../permissions/guard.js';
|
|
8
9
|
import { globalSnapshotManager } from '../core/snapshot.js';
|
|
9
10
|
import { UsageTracker } from '../core/usage.js';
|
|
11
|
+
import { MCPManager } from '../mcp/manager.js';
|
|
10
12
|
export async function runHeadlessMode(userPrompt, config, options = {}) {
|
|
11
13
|
const planMode = !!options.planMode;
|
|
12
14
|
const isYolo = !!options.yolo || !!config.autoApprove;
|
|
@@ -14,13 +16,9 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
|
|
|
14
16
|
const isJson = !!options.json;
|
|
15
17
|
let history = new History();
|
|
16
18
|
const sysPrompt = await buildSystemPrompt(planMode);
|
|
17
|
-
history.
|
|
19
|
+
history.updateSystemPrompt(sysPrompt);
|
|
18
20
|
history.addMessage({ role: 'user', content: userPrompt });
|
|
19
|
-
|
|
20
|
-
const tools = getTools(planMode);
|
|
21
|
-
const guard = new CLIConsoleGuard(isYolo, config.bashAllowlist || []);
|
|
22
|
-
const agent = new Agent(config, provider, tools, history, guard);
|
|
23
|
-
const executedTools = [];
|
|
21
|
+
let executedTools = [];
|
|
24
22
|
let accumulatedText = '';
|
|
25
23
|
let accumulatedReasoning = '';
|
|
26
24
|
let isFatalError = false;
|
|
@@ -34,8 +32,13 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
|
|
|
34
32
|
process.exit(130);
|
|
35
33
|
});
|
|
36
34
|
try {
|
|
35
|
+
await MCPManager.getInstance().init();
|
|
36
|
+
const provider = createProvider(config);
|
|
37
|
+
const tools = getTools(planMode);
|
|
38
|
+
const guard = new CLIConsoleGuard(isYolo);
|
|
39
|
+
const agent = new Agent(config, provider, tools, history, guard);
|
|
37
40
|
if (!isQuiet && !isJson) {
|
|
38
|
-
console.log(pc.bold(pc.cyan(`⚡ devx
|
|
41
|
+
console.log(pc.bold(pc.cyan(`⚡ devx v${DEVX_VERSION} (headless) | ${planMode ? 'PLAN' : 'AGENT'} | ${config.model}`)));
|
|
39
42
|
console.log(pc.dim(`Task: ${userPrompt}\n`));
|
|
40
43
|
}
|
|
41
44
|
for await (const event of agent.run(abortController.signal)) {
|
|
@@ -67,7 +70,6 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
}
|
|
70
|
-
globalSnapshotManager.finishTurn();
|
|
71
73
|
if (isJson) {
|
|
72
74
|
const summary = UsageTracker.getInstance().getSummary();
|
|
73
75
|
const output = {
|
|
@@ -107,4 +109,8 @@ export async function runHeadlessMode(userPrompt, config, options = {}) {
|
|
|
107
109
|
}
|
|
108
110
|
return 1;
|
|
109
111
|
}
|
|
112
|
+
finally {
|
|
113
|
+
globalSnapshotManager.finishTurn();
|
|
114
|
+
MCPManager.getInstance().stopAll();
|
|
115
|
+
}
|
|
110
116
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import fsSync from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import os from 'os';
|
|
5
|
+
export const HISTORY_FILE = path.join(os.homedir(), '.devx', 'history.jsonl');
|
|
6
|
+
const MAX_HISTORY_LINES = 500;
|
|
7
|
+
export class PromptHistoryNavigator {
|
|
8
|
+
history = [];
|
|
9
|
+
cursor = -1;
|
|
10
|
+
savedDraft = '';
|
|
11
|
+
filterQuery = '';
|
|
12
|
+
fuzzy = false;
|
|
13
|
+
constructor(initialHistory = [], fuzzy = false) {
|
|
14
|
+
this.history = [...initialHistory];
|
|
15
|
+
this.cursor = this.history.length;
|
|
16
|
+
this.fuzzy = fuzzy;
|
|
17
|
+
}
|
|
18
|
+
static async loadPersistent() {
|
|
19
|
+
try {
|
|
20
|
+
if (!fsSync.existsSync(HISTORY_FILE)) {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
const raw = await fs.readFile(HISTORY_FILE, 'utf8');
|
|
24
|
+
const lines = raw
|
|
25
|
+
.split('\n')
|
|
26
|
+
.map((l) => l.trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.map((l) => {
|
|
29
|
+
try {
|
|
30
|
+
const parsed = JSON.parse(l);
|
|
31
|
+
return parsed.prompt || parsed.text || l;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return l;
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return lines.slice(-MAX_HISTORY_LINES);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
static async appendPersistent(prompt) {
|
|
44
|
+
try {
|
|
45
|
+
const dir = path.dirname(HISTORY_FILE);
|
|
46
|
+
if (!fsSync.existsSync(dir)) {
|
|
47
|
+
await fs.mkdir(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
const entry = JSON.stringify({ prompt, timestamp: Date.now() }) + '\n';
|
|
50
|
+
await fs.appendFile(HISTORY_FILE, entry, 'utf8');
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
}
|
|
54
|
+
add(entry) {
|
|
55
|
+
const trimmed = entry.trim();
|
|
56
|
+
if (!trimmed)
|
|
57
|
+
return;
|
|
58
|
+
if (this.history.length > 0 && this.history[this.history.length - 1] === trimmed) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.history.push(trimmed);
|
|
62
|
+
if (this.history.length > MAX_HISTORY_LINES) {
|
|
63
|
+
this.history.shift();
|
|
64
|
+
}
|
|
65
|
+
this.cursor = this.history.length;
|
|
66
|
+
this.filterQuery = '';
|
|
67
|
+
PromptHistoryNavigator.appendPersistent(trimmed).catch(() => { });
|
|
68
|
+
}
|
|
69
|
+
getHistory() {
|
|
70
|
+
return [...this.history];
|
|
71
|
+
}
|
|
72
|
+
navigateUp(currentText) {
|
|
73
|
+
if (this.history.length === 0)
|
|
74
|
+
return null;
|
|
75
|
+
if (this.cursor === this.history.length) {
|
|
76
|
+
this.savedDraft = currentText;
|
|
77
|
+
this.filterQuery = currentText.toLowerCase().trim();
|
|
78
|
+
}
|
|
79
|
+
let nextCursor = this.cursor - 1;
|
|
80
|
+
while (nextCursor >= 0) {
|
|
81
|
+
const item = this.history[nextCursor];
|
|
82
|
+
if (!this.fuzzy || !this.filterQuery || item.toLowerCase().includes(this.filterQuery)) {
|
|
83
|
+
this.cursor = nextCursor;
|
|
84
|
+
return item;
|
|
85
|
+
}
|
|
86
|
+
nextCursor--;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
navigateDown() {
|
|
91
|
+
if (this.cursor >= this.history.length)
|
|
92
|
+
return null;
|
|
93
|
+
let nextCursor = this.cursor + 1;
|
|
94
|
+
while (nextCursor < this.history.length) {
|
|
95
|
+
const item = this.history[nextCursor];
|
|
96
|
+
if (!this.fuzzy || !this.filterQuery || item.toLowerCase().includes(this.filterQuery)) {
|
|
97
|
+
this.cursor = nextCursor;
|
|
98
|
+
return item;
|
|
99
|
+
}
|
|
100
|
+
nextCursor++;
|
|
101
|
+
}
|
|
102
|
+
// Reached bottom, restore draft
|
|
103
|
+
this.cursor = this.history.length;
|
|
104
|
+
return this.savedDraft;
|
|
105
|
+
}
|
|
106
|
+
reset() {
|
|
107
|
+
this.cursor = this.history.length;
|
|
108
|
+
this.savedDraft = '';
|
|
109
|
+
this.filterQuery = '';
|
|
110
|
+
}
|
|
111
|
+
}
|