wormclaude 1.0.33 → 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/api.js +2 -0
- package/dist/theme.js +1 -1
- package/dist/tui.js +80 -91
- package/package.json +1 -1
package/dist/api.js
CHANGED
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
|
}
|