wormclaude 1.0.50 → 1.0.52

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/theme.js CHANGED
@@ -16,4 +16,4 @@ export const theme = {
16
16
  synType: '#a78bfa', // tip/sınıf adları, sabitler
17
17
  synProp: '#e0e0e0', // özellik/anahtar adları
18
18
  };
19
- export const VERSION = '1.0.50';
19
+ export const VERSION = '1.0.52';
package/dist/tui.js CHANGED
@@ -64,6 +64,8 @@ export async function runTui() {
64
64
  ];
65
65
  const displayItems = [{ kind: 'banner' }];
66
66
  let inputBuf = '', busy = false, streamChars = 0, spin = 0;
67
+ let streamPreview = ''; // canlı akış önizlemesi (footer'da)
68
+ const PREVIEW_LINES = 6; // akışta sabit önizleme satırı (yükseklik stabil → titreme yok)
67
69
  const SPIN = ['·', '✢', '✳', '✶', '✻', '✽', '✶', '✳', '✢'];
68
70
  // Giriş kutusunu genişliğe göre ÇOK SATIRA sar (uzun mesaj/kod sağa taşmaz, alta iner).
69
71
  const MAX_INPUT_LINES = 10;
@@ -84,22 +86,15 @@ export async function runTui() {
84
86
  return shown.map((ln, i) => (i === 0 ? paint('✶ ', theme.redBright, true) : ' ') + paint(ln, theme.white) + (i === shown.length - 1 ? paint('▌', theme.greyDim) : ''));
85
87
  };
86
88
  // Footer yüksekliği DİNAMİK: izin=4; değilse (durum/menü) + çizgi + giriş-satırları + çizgi.
87
- let prevFH = 4;
88
- const footerHeight = () => {
89
- if (perm)
90
- return 4;
91
- const m = cmdMatches();
92
- const statusLines = m.length ? Math.min(8, m.length) : 1;
93
- return statusLines + 2 + inputBoxLines(Math.max(8, cols())).length;
94
- };
95
- // Yükseklik değiştiyse scroll-region'ı + içeriği yeniden bas; değişmediyse sadece footer'ı çiz.
96
- const refresh = () => { const fh = footerHeight(); if (fh !== prevFH)
97
- redrawAll();
98
- else
99
- drawFooter(); };
89
+ // SABİT rezerve footer alanı (scroll region bir kez ayarlanır → churn/duplicate yok). Footer bu
90
+ // alanın DİBİNE sabitlenir; küçükse üstü boşaltılır. Menü/önizleme/giriş bu sınıra göre kırpılır.
91
+ const FOOTER_MAX = () => Math.min(11, Math.max(5, rows() - 6));
92
+ // refresh = sadece footer'ı çiz (region sabit; içerik yeniden BASILMAZ → duplicate olmaz).
93
+ const refresh = () => drawFooter();
100
94
  setToolConfig(config); // araçlar (Write/Bash/Edit…) aynı config'i kullansın
101
- // İzin (araç onayı) durumu
95
+ // İzin (araç onayı) + soru (AskUserQuestion) durumu
102
96
  let perm = null;
97
+ let ask = null;
103
98
  const allowAll = new Set(); // "Tümüne izin" denen araçlar (oturum boyu)
104
99
  const inputHistory = []; // gönderilen mesajlar (↑/↓ ile geri çağrılır)
105
100
  let histIdx = -1; // -1 = geçmişte gezinmiyor
@@ -163,16 +158,15 @@ export async function runTui() {
163
158
  return [...L, ...R].map((r) => fit(cell(r), W)); // dar ekran → alt alta
164
159
  return [0, 1, 2].map((i) => fit(padVis(cell(L[i]), colW) + cell(R[i]), W)); // geniş → 2 sütun
165
160
  }
166
- // ── Scroll region: üst = içerik (kayar), alt = sabit footer (dinamik yükseklik) ──
161
+ // ── Scroll region: üst=içerik, alt=SABİT rezerve footer alanı. İmleci koru (DECSTBM home'a alır). ──
167
162
  function setRegion() {
168
- const bottom = Math.max(1, rows() - footerHeight() - 1); // footer üstünde 1 boş satır
169
- process.stdout.write(`\x1b[1;${bottom}r`);
163
+ const bottom = Math.max(1, rows() - FOOTER_MAX() - 1);
164
+ process.stdout.write(`\x1b7\x1b[1;${bottom}r\x1b8`); // save · region · restore (içerik imleci bozulmaz)
170
165
  }
171
- // ── Sabit footeren alta mutlak konumla çiz (imleci kaydet/geri yükle içerik bozulmaz) ──
166
+ // ── Footerrezerve alanın DİBİNE çiz; rezerve alanın geri kalanını boşalt (eski menü kalmaz). ──
172
167
  function drawFooter() {
173
168
  const W = Math.max(8, cols());
174
- const fh = footerHeight();
175
- const start = rows() - fh + 1;
169
+ const R = rows(), FM = FOOTER_MAX();
176
170
  const line = paint('─'.repeat(W), theme.red);
177
171
  let body;
178
172
  if (perm) {
@@ -182,10 +176,25 @@ export async function runTui() {
182
176
  + paint('[T] Tümüne izin', theme.grey) + paint(' · ', theme.greyDim) + paint('[H] Hayır', theme.grey);
183
177
  body = [line, q, opts, line];
184
178
  }
179
+ else if (ask) {
180
+ // Soru dialogu (AskUserQuestion): soru + numaralı seçenekler
181
+ const q = paint(' ? ', theme.redBright, true) + paint(ask.question, theme.white);
182
+ const opts = ask.options.map((o, i) => ' ' + paint((i === ask.sel ? '✶ ' : ' ') + (i + 1) + '. ' + o.label, i === ask.sel ? theme.redBright : theme.grey) + (o.description ? paint(' ' + o.description, theme.greyDim) : ''));
183
+ body = [line, q, ...opts, line];
184
+ }
185
185
  else {
186
186
  const inputLines = inputBoxLines(W); // çok-satırlı giriş (sarılmış)
187
187
  const m = cmdMatches();
188
- if (m.length) {
188
+ if (busy && streamPreview) {
189
+ // CANLI akış önizlemesi (son PREVIEW_LINES satır, sabit yükseklik)
190
+ const pl = streamPreview.split('\n');
191
+ const tail = pl.slice(-PREVIEW_LINES);
192
+ while (tail.length < PREVIEW_LINES)
193
+ tail.push('');
194
+ const prev = tail.map((l, i) => (i === 0 ? paint('✶ ', theme.redBright, true) : ' ') + paint(l, theme.white));
195
+ body = [...prev, line, ...inputLines, line];
196
+ }
197
+ else if (m.length) {
189
198
  // DİKEY slash menü — KAYAN pencere (8 satır, seçimi takip eder), seçili kırmızı.
190
199
  const sel = Math.min(cmdSel, m.length - 1);
191
200
  const WIN = 8;
@@ -204,14 +213,19 @@ export async function runTui() {
204
213
  body = [status, line, ...inputLines, line];
205
214
  }
206
215
  }
207
- let out = '\x1b7'; // imleci kaydet
208
- body.forEach((l, i) => { out += `\x1b[${start + i};1H\x1b[2K` + fit(l, W); });
209
- out += '\x1b8'; // imleci geri yükle (içerik alanı)
216
+ if (body.length > FM)
217
+ body = body.slice(body.length - FM); // rezerve alanı aşma (giriş hep görünür)
218
+ const bodyStart = R - body.length + 1; // footer'ı ekranın DİBİNE sabitle
219
+ const reservedTop = R - FM + 1;
220
+ let out = '\x1b7'; // imleci kaydet (içerik alanı)
221
+ for (let r = reservedTop; r < bodyStart; r++)
222
+ out += `\x1b[${r};1H\x1b[2K`; // üst rezerve satırları boşalt
223
+ body.forEach((l, i) => { out += `\x1b[${bodyStart + i};1H\x1b[2K` + fit(l, W); });
224
+ out += '\x1b8'; // imleci geri yükle
210
225
  process.stdout.write(out);
211
226
  }
212
- // ── Her şeyi yeni boyutta yeniden bas (banner/kod responsive); içerik bölgesine ──
227
+ // ── Her şeyi yeni boyutta yeniden bas (SADECE açılış/resize/clear; footer değişiminde DEĞİL). ──
213
228
  function redrawAll() {
214
- prevFH = footerHeight(); // mevcut footer yüksekliğini kaydet (refresh karşılaştırması için)
215
229
  setRegion();
216
230
  process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
217
231
  const W = cols();
@@ -229,15 +243,17 @@ export async function runTui() {
229
243
  async function runTurn(userText, displayText) {
230
244
  busy = true;
231
245
  streamChars = 0;
246
+ streamPreview = '';
232
247
  if (displayText !== undefined)
233
248
  printItem({ kind: 'user', text: displayText }); // skill/@mention: gösterim farklı
234
249
  history.push({ role: 'user', content: userText });
235
- const timer = setInterval(() => { spin++; if (busy && !perm)
250
+ const timer = setInterval(() => { spin++; if (busy && !perm && !ask)
236
251
  refresh(); }, 120);
237
252
  let lastSig = '', sameCount = 0, usedWeb = false, lastAnswer = '';
238
253
  try {
239
254
  for (let iter = 0; iter < 25; iter++) {
240
255
  streamChars = 0;
256
+ streamPreview = '';
241
257
  // Oto-compact: bağlam eşiği aşılınca otomatik özetle (uzun sohbet patlamasın)
242
258
  if (shouldAutoCompact(history)) {
243
259
  try {
@@ -251,11 +267,11 @@ export async function runTui() {
251
267
  let answer = '';
252
268
  let toolCalls = [];
253
269
  for await (const ev of streamChat(history, allToolSchemas(), config)) {
254
- // NOT: token başına drawFooter ÇAĞIRMA (uzun cevapta binlerce çizim titreme/kaybolma).
255
- // streamChars güncellenir; footer'ı 120ms'lik spinTimer çizer; yazma anında keypress çizer.
270
+ // CANLI akış: answer biriktir + streamPreview güncelle. drawFooter'ı 120ms timer çizer (titreme yok).
256
271
  if (ev.type === 'text') {
257
272
  answer += ev.text;
258
273
  streamChars = answer.length;
274
+ streamPreview = cleanModelText(answer);
259
275
  }
260
276
  else if (ev.type === 'done')
261
277
  toolCalls = ev.toolCalls || [];
@@ -263,6 +279,7 @@ export async function runTui() {
263
279
  answer += `\n[hata: ${ev.error}]`;
264
280
  }
265
281
  answer = cleanModelText(answer).trim();
282
+ streamPreview = ''; // canlı önizlemeyi kapat → footer küçülür, formatlı cevap içeriğe basılır
266
283
  const am = { role: 'assistant', content: answer || '' };
267
284
  if (toolCalls.length)
268
285
  am.tool_calls = toolCalls.map((t) => ({ id: t.id, type: 'function', function: { name: t.name, arguments: t.args || '{}' } }));
@@ -292,6 +309,7 @@ export async function runTui() {
292
309
  perm = { label: toolLabel(c.name, args), name: c.name, resolve };
293
310
  refresh();
294
311
  }),
312
+ ask: (q) => new Promise((resolve) => { ask = { question: q.question, options: q.options.length ? q.options : [{ label: 'Tamam' }], sel: 0, resolve }; refresh(); }),
295
313
  onResult: (c, _i, res) => { if ((c.name === 'WebSearch' || c.name === 'WebFetch') && res.ok)
296
314
  usedWeb = true; printItem({ kind: 'tool', label: toolLabel(c.name, res.args), result: sanitizeOutput(res.output), ok: res.ok }); },
297
315
  });
@@ -305,6 +323,8 @@ export async function runTui() {
305
323
  clearInterval(timer);
306
324
  busy = false;
307
325
  perm = null;
326
+ ask = null;
327
+ streamPreview = '';
308
328
  refresh();
309
329
  // Web-öğrenme: web'de arayıp cevap ürettiyse {soru→cevap}'ı eğitim datasına ekle
310
330
  if (usedWeb && lastAnswer) {
@@ -339,6 +359,40 @@ export async function runTui() {
339
359
  catch { } });
340
360
  let ctrlcAt = 0;
341
361
  process.stdin.on('keypress', (str, key) => {
362
+ // Soru dialogu (AskUserQuestion) aktifse: ↑↓ / 1-9 seç, Enter onayla, Esc/Ctrl+C ilk seçenek
363
+ if (ask) {
364
+ const n = ask.options.length;
365
+ if (key && key.name === 'up') {
366
+ ask.sel = (ask.sel - 1 + n) % n;
367
+ refresh();
368
+ return;
369
+ }
370
+ if (key && key.name === 'down') {
371
+ ask.sel = (ask.sel + 1) % n;
372
+ refresh();
373
+ return;
374
+ }
375
+ if (str && /^[1-9]$/.test(str) && +str <= n) {
376
+ ask.sel = +str - 1;
377
+ refresh();
378
+ return;
379
+ }
380
+ if (key && key.name === 'return') {
381
+ const r = ask.resolve, v = ask.options[ask.sel]?.label || '';
382
+ ask = null;
383
+ r(v);
384
+ refresh();
385
+ return;
386
+ }
387
+ if (key && (key.name === 'escape' || (key.ctrl && key.name === 'c'))) {
388
+ const r = ask.resolve, v = ask.options[0]?.label || '';
389
+ ask = null;
390
+ r(v);
391
+ refresh();
392
+ return;
393
+ }
394
+ return;
395
+ }
342
396
  // İzin dialogu aktifse: E=Evet, T=Tümüne izin, H/Esc=Hayır (Ctrl+C de Hayır)
343
397
  if (perm) {
344
398
  const k = (str || '').toLowerCase();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wormclaude",
3
- "version": "1.0.50",
3
+ "version": "1.0.52",
4
4
  "description": "WormClaude CLI - uncensored security+code assistant (ink TUI, Claude-style)",
5
5
  "type": "module",
6
6
  "bin": {