wormclaude 1.0.33 → 1.0.35
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 +83 -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,123 @@ 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
|
+
const W = cols();
|
|
82
|
+
// Sıra: BANNER (+ alt yazı) → model/plan/mail/cwd başlığı → mesajlar.
|
|
83
|
+
process.stdout.write(itemAnsi(displayItems[0], W) + '\n'); // banner (displayItems[0])
|
|
84
|
+
process.stdout.write(headerLines(W).join('\n') + '\n'); // banner ALTINA: model/sürüm/plan/mail/cwd
|
|
85
|
+
for (let i = 1; i < displayItems.length; i++)
|
|
86
|
+
process.stdout.write(itemAnsi(displayItems[i], W) + '\n');
|
|
87
|
+
drawFooter();
|
|
90
88
|
}
|
|
91
|
-
//
|
|
89
|
+
// İçerik öğesini ekle + içerik alanına bas (taşarsa scrollback'e → kopyalanır). Footer sabit kalır.
|
|
90
|
+
const printItem = (it) => { displayItems.push(it); process.stdout.write(itemAnsi(it, cols()) + '\n'); drawFooter(); };
|
|
91
|
+
// ── Bir sohbet turu (M1: araç yok). Akış footer'da karakter sayacı; bitince içeriğe basılır ──
|
|
92
92
|
async function runTurn(userText) {
|
|
93
93
|
busy = true;
|
|
94
|
-
|
|
94
|
+
streamChars = 0;
|
|
95
95
|
history.push({ role: 'user', content: userText });
|
|
96
|
-
const
|
|
97
|
-
|
|
96
|
+
const timer = setInterval(() => { spin++; if (busy)
|
|
97
|
+
drawFooter(); }, 120);
|
|
98
98
|
let answer = '';
|
|
99
99
|
try {
|
|
100
100
|
for await (const ev of streamChat(history, [], config)) {
|
|
101
101
|
if (ev.type === 'text') {
|
|
102
102
|
answer += ev.text;
|
|
103
|
-
|
|
104
|
-
renderFooter();
|
|
103
|
+
streamChars = answer.length;
|
|
105
104
|
}
|
|
106
|
-
else if (ev.type === 'error')
|
|
105
|
+
else if (ev.type === 'error')
|
|
107
106
|
answer += `\n[hata: ${ev.error}]`;
|
|
108
|
-
}
|
|
109
107
|
}
|
|
110
108
|
}
|
|
111
109
|
catch (e) {
|
|
112
110
|
answer += `\n[bağlantı hatası: ${e?.message || e}]`;
|
|
113
111
|
}
|
|
114
|
-
clearInterval(
|
|
112
|
+
clearInterval(timer);
|
|
115
113
|
answer = cleanModelText(answer).trim();
|
|
116
114
|
busy = false;
|
|
117
|
-
streamPreview = '';
|
|
118
115
|
if (answer) {
|
|
119
116
|
history.push({ role: 'assistant', content: answer });
|
|
120
117
|
printItem({ kind: 'assistant', text: answer });
|
|
121
118
|
}
|
|
122
|
-
|
|
119
|
+
drawFooter();
|
|
123
120
|
}
|
|
124
|
-
// ──
|
|
121
|
+
// ── Kurulum ──
|
|
125
122
|
readline.emitKeypressEvents(process.stdin);
|
|
126
123
|
if (process.stdin.isTTY)
|
|
127
124
|
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
125
|
try {
|
|
131
126
|
process.stdout.write('\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1006l\x1b[?1015l\x1b[?1007l');
|
|
132
127
|
}
|
|
133
128
|
catch { }
|
|
134
|
-
|
|
129
|
+
process.stdout.write('\x1b[?25l'); // gerçek imleci gizle (kendi ▌ bloğumuzu çiziyoruz)
|
|
135
130
|
redrawAll();
|
|
136
|
-
|
|
131
|
+
fetchAccount(config).then((a) => { if (a) {
|
|
132
|
+
account = a;
|
|
133
|
+
redrawAll();
|
|
134
|
+
} }).catch(() => { });
|
|
135
|
+
const quit = () => { process.stdout.write('\x1b[r\x1b[?25h\x1b[2J\x1b[3J\x1b[H'); process.exit(0); };
|
|
136
|
+
process.on('exit', () => { try {
|
|
137
|
+
process.stdout.write('\x1b[r\x1b[?25h');
|
|
138
|
+
}
|
|
139
|
+
catch { } });
|
|
137
140
|
let ctrlcAt = 0;
|
|
138
141
|
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
142
|
if (key && key.ctrl && key.name === 'c') {
|
|
142
143
|
if (inputBuf) {
|
|
143
144
|
inputBuf = '';
|
|
144
|
-
|
|
145
|
+
drawFooter();
|
|
145
146
|
return;
|
|
146
147
|
}
|
|
147
148
|
const now = Date.now();
|
|
@@ -157,17 +158,16 @@ export async function runTui() {
|
|
|
157
158
|
}
|
|
158
159
|
if (key && key.name === 'return') {
|
|
159
160
|
if (busy)
|
|
160
|
-
return; // tur sürerken Enter
|
|
161
|
+
return; // tur sürerken Enter beklemede; yazılan metin durur (type-ahead)
|
|
161
162
|
const v = inputBuf.trim();
|
|
162
163
|
inputBuf = '';
|
|
163
164
|
if (!v) {
|
|
164
|
-
|
|
165
|
+
drawFooter();
|
|
165
166
|
return;
|
|
166
167
|
}
|
|
167
168
|
if (v === '/cikis' || v === '/exit' || v === '/quit') {
|
|
168
169
|
quit();
|
|
169
170
|
}
|
|
170
|
-
// /kopyala — son yanıtı OSC52 ile panoya
|
|
171
171
|
if (v === '/kopyala' || v === '/copy') {
|
|
172
172
|
const last = [...history].reverse().find((m) => m.role === 'assistant');
|
|
173
173
|
if (last) {
|
|
@@ -182,32 +182,24 @@ export async function runTui() {
|
|
|
182
182
|
runTurn(v);
|
|
183
183
|
return;
|
|
184
184
|
}
|
|
185
|
-
if (key &&
|
|
185
|
+
if (key && key.name === 'backspace') {
|
|
186
186
|
inputBuf = inputBuf.slice(0, -1);
|
|
187
|
-
|
|
187
|
+
drawFooter();
|
|
188
188
|
return;
|
|
189
189
|
}
|
|
190
190
|
if (key && key.name === 'escape') {
|
|
191
191
|
inputBuf = '';
|
|
192
|
-
|
|
192
|
+
drawFooter();
|
|
193
193
|
return;
|
|
194
194
|
}
|
|
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.
|
|
195
|
+
// sadece gerçek yazdırılabilir karakter (her zaman → cevap üretilirken bile type-ahead)
|
|
198
196
|
if (str && !key?.ctrl && !key?.meta && !str.startsWith('\x1b') && !/[\x00-\x1f]/.test(str)) {
|
|
199
197
|
inputBuf += str;
|
|
200
|
-
|
|
198
|
+
drawFooter();
|
|
201
199
|
}
|
|
202
200
|
});
|
|
203
|
-
// resize → her
|
|
204
|
-
// (100ms) HER ŞEYİ yeni genişlikte yeniden bas (banner/kod responsive, bozulmaz).
|
|
201
|
+
// resize → settle sonrası her şeyi yeni boyutta yeniden bas (region + banner + footer)
|
|
205
202
|
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
|
-
});
|
|
203
|
+
process.stdout.on('resize', () => { if (rzTimer)
|
|
204
|
+
clearTimeout(rzTimer); rzTimer = setTimeout(redrawAll, 100); });
|
|
213
205
|
}
|