tsunami-code 3.12.6 → 3.12.7
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/index.js +1 -1
- package/lib/tools.js +61 -1
- package/lib/ui.js +89 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
import { listMemories, readMemory, saveMemory, deleteMemory, getMemdirPath } from './lib/memdir.js';
|
|
28
28
|
import { execSync, spawn } from 'child_process';
|
|
29
29
|
|
|
30
|
-
const VERSION = '3.12.
|
|
30
|
+
const VERSION = '3.12.7';
|
|
31
31
|
const CONFIG_DIR = join(os.homedir(), '.tsunami-code');
|
|
32
32
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
33
33
|
const DEFAULT_SERVER = 'https://radiometric-reita-amuck.ngrok-free.dev';
|
package/lib/tools.js
CHANGED
|
@@ -14,6 +14,11 @@ const execAsync = promisify(exec);
|
|
|
14
14
|
// ── Lines Changed Tracking ────────────────────────────────────────────────────
|
|
15
15
|
export const linesChanged = { added: 0, removed: 0 };
|
|
16
16
|
|
|
17
|
+
// ── File Changelog Tracking (feature 5) ──────────────────────────────────────
|
|
18
|
+
export const fileChangelog = new Map(); // path → { action: 'created'|'modified'|'deleted', lines: number }
|
|
19
|
+
export function getFileChangelog() { return fileChangelog; }
|
|
20
|
+
export function clearFileChangelog() { fileChangelog.clear(); }
|
|
21
|
+
|
|
17
22
|
// ── Undo Stack (per-turn batching) ───────────────────────────────────────────
|
|
18
23
|
const _undoTurns = [];
|
|
19
24
|
let _currentTurnFiles = [];
|
|
@@ -613,11 +618,49 @@ export const WriteTool = {
|
|
|
613
618
|
try {
|
|
614
619
|
_pushFileSnapshot(file_path);
|
|
615
620
|
const newLineCount = content.split('\n').length;
|
|
616
|
-
const
|
|
621
|
+
const fileExists = existsSync(file_path);
|
|
622
|
+
const oldContent = fileExists ? readFileSync(file_path, 'utf8') : '';
|
|
623
|
+
const oldLineCount = fileExists ? oldContent.split('\n').length : 0;
|
|
624
|
+
|
|
625
|
+
// Feature 8: Show diff and ask confirmation when overwriting an existing file
|
|
626
|
+
if (fileExists && TSUNAMI_MODE !== 'bypass' && TSUNAMI_MODE !== 'accept-edits') {
|
|
627
|
+
const oldLines = oldContent.split('\n');
|
|
628
|
+
const newLines = content.split('\n');
|
|
629
|
+
const diffLines = [];
|
|
630
|
+
const maxLen = Math.max(oldLines.length, newLines.length);
|
|
631
|
+
for (let i = 0; i < maxLen; i++) {
|
|
632
|
+
if (i >= oldLines.length) {
|
|
633
|
+
diffLines.push(`\x1b[32m+ ${newLines[i]}\x1b[0m`);
|
|
634
|
+
} else if (i >= newLines.length) {
|
|
635
|
+
diffLines.push(`\x1b[31m- ${oldLines[i]}\x1b[0m`);
|
|
636
|
+
} else if (oldLines[i] !== newLines[i]) {
|
|
637
|
+
diffLines.push(`\x1b[31m- ${oldLines[i]}\x1b[0m`);
|
|
638
|
+
diffLines.push(`\x1b[32m+ ${newLines[i]}\x1b[0m`);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (diffLines.length > 0) {
|
|
642
|
+
const preview = diffLines.slice(0, 40).join('\n');
|
|
643
|
+
process.stdout.write(`\n${preview}${diffLines.length > 40 ? `\n ... (${diffLines.length - 40} more diff lines)` : ''}\n`);
|
|
644
|
+
}
|
|
645
|
+
const confirmed = await new Promise(resolve => {
|
|
646
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
647
|
+
rl.question(` Write to ${file_path}? [Y/n] `, answer => {
|
|
648
|
+
rl.close();
|
|
649
|
+
resolve(answer.trim().toLowerCase() !== 'n');
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
if (!confirmed) return 'Write cancelled by user';
|
|
653
|
+
}
|
|
654
|
+
|
|
617
655
|
writeFileSync(file_path, content, 'utf8');
|
|
618
656
|
linesChanged.added += Math.max(0, newLineCount - oldLineCount);
|
|
619
657
|
linesChanged.removed += Math.max(0, oldLineCount - newLineCount);
|
|
620
658
|
recordWriteAudit(file_path, 'Write', `${newLineCount} lines`);
|
|
659
|
+
|
|
660
|
+
// Feature 5: track file changelog
|
|
661
|
+
const resolved = safeResolve(file_path);
|
|
662
|
+
fileChangelog.set(resolved, { action: fileExists ? 'modified' : 'created', lines: newLineCount });
|
|
663
|
+
|
|
621
664
|
return `Written ${newLineCount} lines to ${file_path}`;
|
|
622
665
|
} catch (e) {
|
|
623
666
|
return `Error writing file: ${e.message}`;
|
|
@@ -1059,20 +1102,37 @@ IMPORTANT: You can call Agent multiple times in one response to run tasks truly
|
|
|
1059
1102
|
if (!_currentServerUrl) return 'Error: AgentTool not initialized (no server URL)';
|
|
1060
1103
|
|
|
1061
1104
|
const url = serverUrl || _currentServerUrl;
|
|
1105
|
+
const taskPreview = task.slice(0, 60).replace(/\n/g, ' ');
|
|
1106
|
+
process.stdout.write(`\n ◎ Agent spawning: ${taskPreview}${task.length > 60 ? '...' : ''}\n`);
|
|
1107
|
+
|
|
1062
1108
|
const subMessages = [
|
|
1063
1109
|
{ role: 'system', content: _agentSystemPrompt || 'You are a capable software engineering sub-agent. Complete the given task fully and return a summary of what you did.' },
|
|
1064
1110
|
{ role: 'user', content: task }
|
|
1065
1111
|
];
|
|
1066
1112
|
|
|
1067
1113
|
const outputTokens = [];
|
|
1114
|
+
const frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'];
|
|
1115
|
+
let frameIdx = 0;
|
|
1116
|
+
const spinInterval = setInterval(() => {
|
|
1117
|
+
process.stdout.write(`\r ◎ Agent working ${frames[frameIdx++ % frames.length]} `);
|
|
1118
|
+
}, 500);
|
|
1119
|
+
|
|
1068
1120
|
try {
|
|
1069
1121
|
await _agentLoopRef(url, subMessages, (token) => { outputTokens.push(token); }, () => {}, null, null, 10);
|
|
1070
1122
|
} catch (e) {
|
|
1123
|
+
clearInterval(spinInterval);
|
|
1124
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
1071
1125
|
return `Sub-agent error: ${e.message}`;
|
|
1072
1126
|
}
|
|
1073
1127
|
|
|
1128
|
+
clearInterval(spinInterval);
|
|
1129
|
+
process.stdout.write('\r' + ' '.repeat(40) + '\r');
|
|
1130
|
+
|
|
1074
1131
|
const lastAssistant = [...subMessages].reverse().find(m => m.role === 'assistant');
|
|
1075
1132
|
const result = lastAssistant?.content || outputTokens.join('');
|
|
1133
|
+
const resultPreview = result.slice(0, 100).replace(/\n/g, ' ');
|
|
1134
|
+
process.stdout.write(` ◎ Agent done — ${resultPreview}${result.length > 100 ? '...' : ''}\n`);
|
|
1135
|
+
|
|
1076
1136
|
return `[Sub-agent result]\n${String(result).slice(0, 6000)}`;
|
|
1077
1137
|
}
|
|
1078
1138
|
};
|
package/lib/ui.js
CHANGED
|
@@ -28,6 +28,9 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
|
|
|
28
28
|
let processing = false;
|
|
29
29
|
let lineHandler = null;
|
|
30
30
|
let _interrupted = false;
|
|
31
|
+
let _abortController = null;
|
|
32
|
+
let vimMode = false;
|
|
33
|
+
let vimInsert = true; // starts in insert mode
|
|
31
34
|
|
|
32
35
|
const rows = () => process.stdout.rows || 24;
|
|
33
36
|
const cols = () => process.stdout.columns || 80;
|
|
@@ -138,8 +141,83 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
|
|
|
138
141
|
return;
|
|
139
142
|
}
|
|
140
143
|
|
|
144
|
+
// Escape — pure ESC (not followed by [ which would be an arrow/function key)
|
|
145
|
+
if (key === '\x1b') {
|
|
146
|
+
if (processing) {
|
|
147
|
+
_interrupted = true;
|
|
148
|
+
if (_abortController) _abortController.abort();
|
|
149
|
+
process.stdout.write(dim('\n ✗ Interrupted\n'));
|
|
150
|
+
} else if (vimMode) {
|
|
151
|
+
// Enter vim normal mode
|
|
152
|
+
vimInsert = false;
|
|
153
|
+
drawBox();
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
141
158
|
if (processing) return;
|
|
142
159
|
|
|
160
|
+
// Vim normal mode key handling
|
|
161
|
+
if (vimMode && !vimInsert) {
|
|
162
|
+
switch (key) {
|
|
163
|
+
case 'i':
|
|
164
|
+
vimInsert = true;
|
|
165
|
+
drawBox();
|
|
166
|
+
return;
|
|
167
|
+
case 'a':
|
|
168
|
+
vimInsert = true;
|
|
169
|
+
cursorPos = Math.min(inputBuf.length, cursorPos + 1);
|
|
170
|
+
drawBox();
|
|
171
|
+
return;
|
|
172
|
+
case 'h':
|
|
173
|
+
cursorPos = Math.max(0, cursorPos - 1);
|
|
174
|
+
drawBox();
|
|
175
|
+
return;
|
|
176
|
+
case 'l':
|
|
177
|
+
cursorPos = Math.min(inputBuf.length, cursorPos + 1);
|
|
178
|
+
drawBox();
|
|
179
|
+
return;
|
|
180
|
+
case 'w': {
|
|
181
|
+
// jump to start of next word
|
|
182
|
+
let i = cursorPos;
|
|
183
|
+
// skip current word chars
|
|
184
|
+
while (i < inputBuf.length && inputBuf[i] !== ' ') i++;
|
|
185
|
+
// skip spaces
|
|
186
|
+
while (i < inputBuf.length && inputBuf[i] === ' ') i++;
|
|
187
|
+
cursorPos = i;
|
|
188
|
+
drawBox();
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
case 'b': {
|
|
192
|
+
// jump to start of previous word
|
|
193
|
+
let i = cursorPos - 1;
|
|
194
|
+
// skip spaces before cursor
|
|
195
|
+
while (i > 0 && inputBuf[i - 1] === ' ') i--;
|
|
196
|
+
// skip word chars
|
|
197
|
+
while (i > 0 && inputBuf[i - 1] !== ' ') i--;
|
|
198
|
+
cursorPos = Math.max(0, i);
|
|
199
|
+
drawBox();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
case '0':
|
|
203
|
+
cursorPos = 0;
|
|
204
|
+
drawBox();
|
|
205
|
+
return;
|
|
206
|
+
case '$':
|
|
207
|
+
cursorPos = inputBuf.length;
|
|
208
|
+
drawBox();
|
|
209
|
+
return;
|
|
210
|
+
case 'x':
|
|
211
|
+
if (cursorPos < inputBuf.length) {
|
|
212
|
+
inputBuf = inputBuf.slice(0, cursorPos) + inputBuf.slice(cursorPos + 1);
|
|
213
|
+
drawBox();
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
default:
|
|
217
|
+
return; // ignore unmapped keys in normal mode
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
143
221
|
// Enter
|
|
144
222
|
if (key === '\r' || key === '\n') {
|
|
145
223
|
const line = inputBuf;
|
|
@@ -417,6 +495,16 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
|
|
|
417
495
|
return v;
|
|
418
496
|
}
|
|
419
497
|
|
|
498
|
+
function setAbortController(ac) {
|
|
499
|
+
_abortController = ac;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function setVimMode(val) {
|
|
503
|
+
vimMode = val;
|
|
504
|
+
vimInsert = true; // always start in insert mode when toggling
|
|
505
|
+
if (!processing) drawBox();
|
|
506
|
+
}
|
|
507
|
+
|
|
420
508
|
function exitUI() {
|
|
421
509
|
resetScrollRegion();
|
|
422
510
|
const r = rows();
|
|
@@ -436,6 +524,6 @@ export function createUI({ planMode: initPlanMode = false, onLine, onTab, onExit
|
|
|
436
524
|
start, pause, resume,
|
|
437
525
|
setPlanMode, setContinuation, setModelLabel, setModeLabel,
|
|
438
526
|
readLine, readChar, selectFromList,
|
|
439
|
-
wasInterrupted, stop, exitUI,
|
|
527
|
+
wasInterrupted, setAbortController, setVimMode, stop, exitUI,
|
|
440
528
|
};
|
|
441
529
|
}
|