wormclaude 1.0.32 → 1.0.34
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/dist/ansi.js +2 -2
- package/dist/api.js +2 -0
- package/dist/cli.js +5 -5
- package/dist/theme.js +1 -1
- package/dist/tui.js +80 -91
- package/package.json +1 -1
package/dist/ansi.js
CHANGED
|
@@ -151,11 +151,11 @@ export function itemAnsi(it, cols) {
|
|
|
151
151
|
}
|
|
152
152
|
if (it.kind === 'assistant') {
|
|
153
153
|
const md = markdownAnsi(it.text || '', cols).split('\n');
|
|
154
|
-
return '\n' + md.map((ln, i) => (i === 0 ? paint('
|
|
154
|
+
return '\n' + md.map((ln, i) => (i === 0 ? paint('› ', theme.redBright, true) + ln : ' ' + ln)).join('\n');
|
|
155
155
|
}
|
|
156
156
|
if (it.kind === 'tool') {
|
|
157
157
|
const n = (it.result || '').split('\n').length, chars = (it.result || '').length;
|
|
158
|
-
const head = paint('
|
|
158
|
+
const head = paint('› ', theme.redBright, true) + paint(it.label || '', theme.white);
|
|
159
159
|
const sub = paint(' ⎿ ', theme.greyDim) + (it.ok
|
|
160
160
|
? paint(`${n} ${t('common.lines') || 'satır'} (${chars})`, theme.grey)
|
|
161
161
|
: paint('✗ ' + (it.result || '').slice(0, 160), theme.errorRed));
|
package/dist/api.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -276,11 +276,11 @@ function buildLines(items, cols) {
|
|
|
276
276
|
}
|
|
277
277
|
else if (it.kind === 'assistant') {
|
|
278
278
|
const seg = mdToSegLines(it.text, W - 2);
|
|
279
|
-
seg.forEach((segs, i) => lines.push(i === 0 ? [{ text: '
|
|
279
|
+
seg.forEach((segs, i) => lines.push(i === 0 ? [{ text: '› ', color: redB, bold: true }, ...segs] : [{ text: ' ', dim: true }, ...segs]));
|
|
280
280
|
}
|
|
281
281
|
else if (it.kind === 'tool') {
|
|
282
282
|
const n = it.result.split('\n').length, chars = it.result.length;
|
|
283
|
-
lines.push([{ text: '
|
|
283
|
+
lines.push([{ text: '› ', color: redB, bold: true }, { text: it.label, color: white }]);
|
|
284
284
|
lines.push([{ text: ' ⎿ ', dim: true }, { text: it.ok ? `${n} satır (${chars} karakter)` : '✗ ' + it.result.slice(0, 120), color: it.ok ? theme.grey : theme.errorRed }]);
|
|
285
285
|
}
|
|
286
286
|
else if (it.kind === 'note') {
|
|
@@ -321,7 +321,7 @@ function RenderItem({ item }) {
|
|
|
321
321
|
}
|
|
322
322
|
if (item.kind === 'assistant') {
|
|
323
323
|
return (React.createElement(Box, { marginTop: 1, flexDirection: "row" },
|
|
324
|
-
React.createElement(Text, { color: theme.redBright, bold: true }, "\
|
|
324
|
+
React.createElement(Text, { color: theme.redBright, bold: true }, "\u203A "),
|
|
325
325
|
React.createElement(Box, { flexDirection: "column", flexGrow: 1 },
|
|
326
326
|
React.createElement(MarkdownDisplay, { text: item.text }))));
|
|
327
327
|
}
|
|
@@ -330,7 +330,7 @@ function RenderItem({ item }) {
|
|
|
330
330
|
const chars = item.result.length;
|
|
331
331
|
return (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
332
332
|
React.createElement(Box, null,
|
|
333
|
-
React.createElement(Text, { color: theme.redBright, bold: true }, "\
|
|
333
|
+
React.createElement(Text, { color: theme.redBright, bold: true }, "\u203A "),
|
|
334
334
|
React.createElement(Text, { color: theme.white }, item.label)),
|
|
335
335
|
React.createElement(Box, null,
|
|
336
336
|
React.createElement(Text, { color: theme.greyDim }, " \u23BF "),
|
|
@@ -1000,7 +1000,7 @@ function App() {
|
|
|
1000
1000
|
off,
|
|
1001
1001
|
" sat\u0131r \u00B7 PageDown / Son") : null,
|
|
1002
1002
|
streaming && off === 0 ? (React.createElement(Box, { flexDirection: "row" },
|
|
1003
|
-
React.createElement(Text, { color: theme.redBright, bold: true }, "\
|
|
1003
|
+
React.createElement(Text, { color: theme.redBright, bold: true }, "\u203A "),
|
|
1004
1004
|
React.createElement(Box, { flexDirection: "column" },
|
|
1005
1005
|
React.createElement(Text, { color: theme.white }, streaming)))) : null));
|
|
1006
1006
|
})()),
|
package/dist/theme.js
CHANGED
package/dist/tui.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// Kendi renderer (ink YOK) — Milestone
|
|
2
|
-
// Mimari:
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Kendi renderer (ink YOK) — Milestone 1b: scroll-region + sabit-dip giriş kutusu.
|
|
2
|
+
// Mimari: terminal SCROLL REGION (DECSTBM) ile ekran ikiye bölünür — üstte içerik (banner/header/
|
|
3
|
+
// mesajlar) kayar, ALTTA giriş kutusu SABİT kalır. İçerik bölgesi tepeden taşınca terminal
|
|
4
|
+
// scrollback'ine gider (fareyle kopyalama). Giriş kutusu mutlak konumla (save/restore imleç)
|
|
5
|
+
// çizilir. Renkli kod + markdown: ansi.ts. WORMCLAUDE_TUI=1 ile çalışır; ink sürümü dokunulmaz.
|
|
5
6
|
import readline from 'node:readline';
|
|
6
|
-
import logUpdate from 'log-update';
|
|
7
7
|
import stringWidth from 'string-width';
|
|
8
|
-
import { loadConfig, streamChat } from './api.js';
|
|
8
|
+
import { loadConfig, streamChat, fetchAccount } from './api.js';
|
|
9
9
|
import { itemAnsi } from './ansi.js';
|
|
10
10
|
import { theme, VERSION } from './theme.js';
|
|
11
11
|
import { cleanModelText } from './textclean.js';
|
|
@@ -13,8 +13,8 @@ const RESET = '\x1b[0m';
|
|
|
13
13
|
const hex = (h) => { const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(h); return m ? `\x1b[38;2;${parseInt(m[1], 16)};${parseInt(m[2], 16)};${parseInt(m[3], 16)}m` : ''; };
|
|
14
14
|
const paint = (s, c, bold = false) => `${bold ? '\x1b[1m' : ''}${c ? hex(c) : ''}${s}${RESET}`;
|
|
15
15
|
const cols = () => process.stdout.columns || 80;
|
|
16
|
-
|
|
17
|
-
//
|
|
16
|
+
const rows = () => process.stdout.rows || 24;
|
|
17
|
+
// Satırı genişliğe ANSI-korumalı kırp (sarma olmaz → sabit footer şaşmaz).
|
|
18
18
|
function fit(line, max) {
|
|
19
19
|
let out = '', w = 0, i = 0;
|
|
20
20
|
while (i < line.length) {
|
|
@@ -26,122 +26,120 @@ function fit(line, max) {
|
|
|
26
26
|
continue;
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
const
|
|
30
|
-
const cw = stringWidth(ch) || 1;
|
|
29
|
+
const cw = stringWidth(line[i]) || 1;
|
|
31
30
|
if (w + cw > max)
|
|
32
31
|
break;
|
|
33
|
-
out +=
|
|
32
|
+
out += line[i];
|
|
34
33
|
w += cw;
|
|
35
34
|
i++;
|
|
36
35
|
}
|
|
37
36
|
return out + RESET;
|
|
38
37
|
}
|
|
39
|
-
// Görünür (ANSI'siz) genişlik
|
|
40
38
|
const vis = (s) => stringWidth(s.replace(/\x1b\[[0-9;]*m/g, ''));
|
|
41
39
|
export async function runTui() {
|
|
42
40
|
const config = loadConfig();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
let
|
|
47
|
-
let spin = 0;
|
|
41
|
+
let account = { plan: '', email: '', name: '' };
|
|
42
|
+
const history = [];
|
|
43
|
+
const displayItems = [{ kind: 'banner' }];
|
|
44
|
+
let inputBuf = '', busy = false, streamChars = 0, spin = 0;
|
|
48
45
|
const SPIN = ['·', '✢', '✳', '✶', '✻', '✽', '✶', '✳', '✢'];
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
renderFooter();
|
|
46
|
+
const FOOTER_H = 2; // giriş satırı + ipucu satırı
|
|
47
|
+
// ── Claude tarzı başlık (model/plan/mail/cwd) — responsive ──
|
|
48
|
+
function headerLines(W) {
|
|
49
|
+
const a = paint(' WormClaude ', theme.greyDim) + paint('v' + VERSION, theme.red, true)
|
|
50
|
+
+ paint(' · model: ', theme.greyDim) + paint(config.model, theme.redBright)
|
|
51
|
+
+ (account.plan ? paint(' · plan: ', theme.greyDim) + paint(account.plan, theme.white) : '');
|
|
52
|
+
const b = paint(' ' + (account.email ? account.email + ' · ' : '') + process.cwd(), theme.greyDim);
|
|
53
|
+
return [a, b].map((l) => fit(l, W));
|
|
54
|
+
}
|
|
55
|
+
// ── Scroll region: üst = içerik (kayar), alt = sabit footer ──
|
|
56
|
+
function setRegion() {
|
|
57
|
+
const bottom = Math.max(1, rows() - FOOTER_H - 1); // footer üstünde 1 boş satır
|
|
58
|
+
process.stdout.write(`\x1b[1;${bottom}r`);
|
|
63
59
|
}
|
|
64
|
-
// ──
|
|
65
|
-
function
|
|
60
|
+
// ── Sabit footer'ı en alta mutlak konumla çiz (imleci kaydet/geri yükle → içerik bozulmaz) ──
|
|
61
|
+
function drawFooter() {
|
|
66
62
|
const W = Math.max(8, cols());
|
|
67
|
-
const
|
|
68
|
-
if (busy && !streamPreview)
|
|
69
|
-
lines.push(paint(`${SPIN[spin % SPIN.length]} `, theme.red) + paint('yanıt bekleniyor…', theme.grey));
|
|
70
|
-
if (streamPreview) {
|
|
71
|
-
const maxTail = Math.max(2, Math.min(12, (process.stdout.rows || 24) - 4)); // ekran boyunu aşma
|
|
72
|
-
const tail = streamPreview.split('\n').slice(-maxTail);
|
|
73
|
-
lines.push(paint('⏺ ', theme.redBright, true) + paint(tail.shift() || '', theme.white));
|
|
74
|
-
for (const l of tail)
|
|
75
|
-
lines.push(' ' + paint(l, theme.white));
|
|
76
|
-
}
|
|
77
|
-
lines.push('');
|
|
78
|
-
// Giriş satırı: uzunsa SONU göster (yazdıkça imleç görünür kalsın)
|
|
79
|
-
const prompt = paint('❯ ', theme.redBright, true);
|
|
63
|
+
const start = rows() - FOOTER_H + 1;
|
|
80
64
|
let shown = inputBuf;
|
|
81
|
-
const avail = W - 3;
|
|
65
|
+
const avail = W - 3;
|
|
82
66
|
if (vis(shown) > avail)
|
|
83
67
|
shown = '…' + shown.slice(-(avail - 1));
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
68
|
+
const inputLine = paint('❯ ', theme.redBright, true) + shown + paint('▌', theme.greyDim);
|
|
69
|
+
const hint = busy
|
|
70
|
+
? paint(`${SPIN[spin % SPIN.length]} yanıt geliyor… ${streamChars} karakter`, theme.grey)
|
|
71
|
+
: paint(' /kopyala panoya · /help komutlar · Ctrl+C çıkış', theme.greyDim);
|
|
72
|
+
let out = '\x1b7'; // imleci kaydet
|
|
73
|
+
[fit(inputLine, W), fit(hint, W)].forEach((l, i) => { out += `\x1b[${start + i};1H\x1b[2K` + l; });
|
|
74
|
+
out += '\x1b8'; // imleci geri yükle (içerik alanı)
|
|
75
|
+
process.stdout.write(out);
|
|
76
|
+
}
|
|
77
|
+
// ── Her şeyi yeni boyutta yeniden bas (banner/kod responsive); içerik bölgesine ──
|
|
78
|
+
function redrawAll() {
|
|
79
|
+
setRegion();
|
|
80
|
+
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
81
|
+
process.stdout.write(headerLines(cols()).join('\n') + '\n');
|
|
82
|
+
for (const it of displayItems)
|
|
83
|
+
process.stdout.write(itemAnsi(it, cols()) + '\n');
|
|
84
|
+
drawFooter();
|
|
90
85
|
}
|
|
91
|
-
//
|
|
86
|
+
// İçerik öğesini ekle + içerik alanına bas (taşarsa scrollback'e → kopyalanır). Footer sabit kalır.
|
|
87
|
+
const printItem = (it) => { displayItems.push(it); process.stdout.write(itemAnsi(it, cols()) + '\n'); drawFooter(); };
|
|
88
|
+
// ── Bir sohbet turu (M1: araç yok). Akış footer'da karakter sayacı; bitince içeriğe basılır ──
|
|
92
89
|
async function runTurn(userText) {
|
|
93
90
|
busy = true;
|
|
94
|
-
|
|
91
|
+
streamChars = 0;
|
|
95
92
|
history.push({ role: 'user', content: userText });
|
|
96
|
-
const
|
|
97
|
-
|
|
93
|
+
const timer = setInterval(() => { spin++; if (busy)
|
|
94
|
+
drawFooter(); }, 120);
|
|
98
95
|
let answer = '';
|
|
99
96
|
try {
|
|
100
97
|
for await (const ev of streamChat(history, [], config)) {
|
|
101
98
|
if (ev.type === 'text') {
|
|
102
99
|
answer += ev.text;
|
|
103
|
-
|
|
104
|
-
renderFooter();
|
|
100
|
+
streamChars = answer.length;
|
|
105
101
|
}
|
|
106
|
-
else if (ev.type === 'error')
|
|
102
|
+
else if (ev.type === 'error')
|
|
107
103
|
answer += `\n[hata: ${ev.error}]`;
|
|
108
|
-
}
|
|
109
104
|
}
|
|
110
105
|
}
|
|
111
106
|
catch (e) {
|
|
112
107
|
answer += `\n[bağlantı hatası: ${e?.message || e}]`;
|
|
113
108
|
}
|
|
114
|
-
clearInterval(
|
|
109
|
+
clearInterval(timer);
|
|
115
110
|
answer = cleanModelText(answer).trim();
|
|
116
111
|
busy = false;
|
|
117
|
-
streamPreview = '';
|
|
118
112
|
if (answer) {
|
|
119
113
|
history.push({ role: 'assistant', content: answer });
|
|
120
114
|
printItem({ kind: 'assistant', text: answer });
|
|
121
115
|
}
|
|
122
|
-
|
|
116
|
+
drawFooter();
|
|
123
117
|
}
|
|
124
|
-
// ──
|
|
118
|
+
// ── Kurulum ──
|
|
125
119
|
readline.emitKeypressEvents(process.stdin);
|
|
126
120
|
if (process.stdin.isTTY)
|
|
127
121
|
process.stdin.setRawMode(true);
|
|
128
|
-
// TÜM mouse-reporting modlarını kapat (ink oturumundan ?1007h sızmış olabilir). Kapalıyken
|
|
129
|
-
// fare sürükleme/seçimi tamamen terminalin işi olur → yukarı sürükleyip seçmek çalışır.
|
|
130
122
|
try {
|
|
131
123
|
process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l\x1b[?1007l');
|
|
132
124
|
}
|
|
133
125
|
catch { }
|
|
134
|
-
|
|
126
|
+
process.stdout.write('\x1b[?25l'); // gerçek imleci gizle (kendi ▌ bloğumuzu çiziyoruz)
|
|
135
127
|
redrawAll();
|
|
136
|
-
|
|
128
|
+
fetchAccount(config).then((a) => { if (a) {
|
|
129
|
+
account = a;
|
|
130
|
+
redrawAll();
|
|
131
|
+
} }).catch(() => { });
|
|
132
|
+
const quit = () => { process.stdout.write('\x1b[r\x1b[?25h\x1b[2J\x1b[3J\x1b[H'); process.exit(0); };
|
|
133
|
+
process.on('exit', () => { try {
|
|
134
|
+
process.stdout.write('\x1b[r\x1b[?25h');
|
|
135
|
+
}
|
|
136
|
+
catch { } });
|
|
137
137
|
let ctrlcAt = 0;
|
|
138
138
|
process.stdin.on('keypress', (str, key) => {
|
|
139
|
-
// Ctrl+C tek başına ÇIKMAZ (Windows'ta seçimi Ctrl+C ile kopyalarken uygulama kapanmasın).
|
|
140
|
-
// Giriş varsa temizler; boşsa 2 sn içinde ikinci Ctrl+C ile çıkar.
|
|
141
139
|
if (key && key.ctrl && key.name === 'c') {
|
|
142
140
|
if (inputBuf) {
|
|
143
141
|
inputBuf = '';
|
|
144
|
-
|
|
142
|
+
drawFooter();
|
|
145
143
|
return;
|
|
146
144
|
}
|
|
147
145
|
const now = Date.now();
|
|
@@ -157,17 +155,16 @@ export async function runTui() {
|
|
|
157
155
|
}
|
|
158
156
|
if (key && key.name === 'return') {
|
|
159
157
|
if (busy)
|
|
160
|
-
return; // tur sürerken Enter
|
|
158
|
+
return; // tur sürerken Enter beklemede; yazılan metin durur (type-ahead)
|
|
161
159
|
const v = inputBuf.trim();
|
|
162
160
|
inputBuf = '';
|
|
163
161
|
if (!v) {
|
|
164
|
-
|
|
162
|
+
drawFooter();
|
|
165
163
|
return;
|
|
166
164
|
}
|
|
167
165
|
if (v === '/cikis' || v === '/exit' || v === '/quit') {
|
|
168
166
|
quit();
|
|
169
167
|
}
|
|
170
|
-
// /kopyala — son yanıtı OSC52 ile panoya
|
|
171
168
|
if (v === '/kopyala' || v === '/copy') {
|
|
172
169
|
const last = [...history].reverse().find((m) => m.role === 'assistant');
|
|
173
170
|
if (last) {
|
|
@@ -182,32 +179,24 @@ export async function runTui() {
|
|
|
182
179
|
runTurn(v);
|
|
183
180
|
return;
|
|
184
181
|
}
|
|
185
|
-
if (key &&
|
|
182
|
+
if (key && key.name === 'backspace') {
|
|
186
183
|
inputBuf = inputBuf.slice(0, -1);
|
|
187
|
-
|
|
184
|
+
drawFooter();
|
|
188
185
|
return;
|
|
189
186
|
}
|
|
190
187
|
if (key && key.name === 'escape') {
|
|
191
188
|
inputBuf = '';
|
|
192
|
-
|
|
189
|
+
drawFooter();
|
|
193
190
|
return;
|
|
194
191
|
}
|
|
195
|
-
//
|
|
196
|
-
// tuşları, fonksiyon tuşları) girişi BOZMAZ ve footer'ı yeniden çizdirmez → fareyle seçim
|
|
197
|
-
// sırasında ekran en alta snap edip seçimi düşürmez.
|
|
192
|
+
// sadece gerçek yazdırılabilir karakter (her zaman → cevap üretilirken bile type-ahead)
|
|
198
193
|
if (str && !key?.ctrl && !key?.meta && !str.startsWith('\x1b') && !/[\x00-\x1f]/.test(str)) {
|
|
199
194
|
inputBuf += str;
|
|
200
|
-
|
|
195
|
+
drawFooter();
|
|
201
196
|
}
|
|
202
197
|
});
|
|
203
|
-
// resize → her
|
|
204
|
-
// (100ms) HER ŞEYİ yeni genişlikte yeniden bas (banner/kod responsive, bozulmaz).
|
|
198
|
+
// resize → settle sonrası her şeyi yeni boyutta yeniden bas (region + banner + footer)
|
|
205
199
|
let rzTimer = null;
|
|
206
|
-
process.stdout.on('resize', () => {
|
|
207
|
-
|
|
208
|
-
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
209
|
-
if (rzTimer)
|
|
210
|
-
clearTimeout(rzTimer);
|
|
211
|
-
rzTimer = setTimeout(redrawAll, 100);
|
|
212
|
-
});
|
|
200
|
+
process.stdout.on('resize', () => { if (rzTimer)
|
|
201
|
+
clearTimeout(rzTimer); rzTimer = setTimeout(redrawAll, 100); });
|
|
213
202
|
}
|