zames_pro 2.8.0 → 2.9.0

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.
@@ -80,6 +80,14 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
80
80
  // chatty model.
81
81
  let looksDoneRetries = 0;
82
82
  const MAX_LOOKSDONE_RETRIES = 3;
83
+ // Watchdog against the agent emitting a tool call and then going silent.
84
+ // After a tool result the expected next answer is a fresh tool call; if we
85
+ // instead get an EMPTY answer or the EXACT same answer as the previous turn
86
+ // (a sign the new message was not sent), we nudge instead of stopping.
87
+ let lastRaw = '';
88
+ let justRanTool = false;
89
+ let watchdogRetries = 0;
90
+ const MAX_WATCHDOG_RETRIES = 3;
83
91
  for (let i = 0; i < maxIterations; i++) {
84
92
  onThinking();
85
93
  // The first message (task) is user input: no throttle.
@@ -97,6 +105,23 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
97
105
  transcript?.log('user_aborted');
98
106
  return rawResponse;
99
107
  }
108
+ // Watchdog: after a tool result we expect a FRESH tool call. If the answer
109
+ // is empty or identical to the previous turn (the new message was not
110
+ // sent), nudge instead of stopping.
111
+ const wdEmpty = !String(rawResponse || '').trim();
112
+ const wdStale = justRanTool && lastRaw.trim() !== '' && rawResponse.trim() === lastRaw.trim();
113
+ if (!isFirst && (wdEmpty || wdStale) && watchdogRetries < MAX_WATCHDOG_RETRIES) {
114
+ watchdogRetries++;
115
+ transcript?.log('watchdog_nudge', {
116
+ attempt: watchdogRetries,
117
+ empty: wdEmpty,
118
+ stale: wdStale,
119
+ response: String(rawResponse || '').slice(0, 200),
120
+ });
121
+ await new Promise((r) => setTimeout(r, 1500));
122
+ continue;
123
+ }
124
+ lastRaw = rawResponse;
100
125
  const parsed = parseToolCall(rawResponse);
101
126
  if (parsed) {
102
127
  const thought = extractPreToolText(rawResponse);
@@ -259,6 +284,10 @@ export async function runAgentLoop({ browser, tools, task, workdir, maxIteration
259
284
  });
260
285
  results.push({ tool: call.tool, result });
261
286
  }
287
+ // A tool just ran — the next answer is expected to be a fresh tool call.
288
+ // Reset the watchdog so the next empty/repeated answer is nudged.
289
+ justRanTool = true;
290
+ watchdogRetries = 0;
262
291
  if (results.length === 1) {
263
292
  const r = results[0];
264
293
  const resultStr = typeof r.result === 'string' ? r.result : JSON.stringify(r.result);
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs/promises';
2
2
  import path from 'path';
3
+ import os from 'os';
3
4
  import { execFileSync } from 'child_process';
4
5
  export const IMAGE_EXTS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico'];
5
6
  export const MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
@@ -185,11 +186,42 @@ export async function saveToTemp(tmpDir, name, data) {
185
186
  // the current platform. Returns the raw bytes plus a short note about which
186
187
  // tool was used (or the reason nothing was found) — the caller shows that note
187
188
  // to the user so a failed paste is never silent.
189
+ // True when running inside WSL. There the X11 clipboard is a SEPARATE
190
+ // clipboard from the Windows one: the user copies an image in Windows, so we
191
+ // must read it through powershell.exe, not xclip.
192
+ export function isWsl() {
193
+ if (process.platform !== 'linux')
194
+ return false;
195
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP)
196
+ return true;
197
+ try {
198
+ const rel = os.release().toLowerCase();
199
+ if (rel.includes('microsoft') || rel.includes('wsl'))
200
+ return true;
201
+ }
202
+ catch { }
203
+ return false;
204
+ }
205
+ // PowerShell one-liner: write the clipboard image as raw PNG bytes to stdout.
206
+ const PS_GET_IMAGE = 'Add-Type -AssemblyName System.Windows.Forms,System.Drawing; ' +
207
+ '$img=[System.Windows.Forms.Clipboard]::GetImage(); ' +
208
+ 'if($img){$ms=New-Object System.IO.MemoryStream; ' +
209
+ '$img.Save($ms,[System.Drawing.Imaging.ImageFormat]::Png); ' +
210
+ '[Console]::OpenStandardOutput().Write($ms.ToArray(),0,$ms.Length)}';
211
+ // PowerShell one-liner: write the clipboard file paths (CF_HDROP) to stdout.
212
+ const PS_GET_FILE_DROP = 'Add-Type -AssemblyName System.Windows.Forms; ' +
213
+ '$d=[System.Windows.Forms.Clipboard]::GetDataObject(); ' +
214
+ 'if($d -and $d.GetDataPresent([System.Windows.Forms.DataFormats]::FileDrop)){' +
215
+ '$f=$d.GetData([System.Windows.Forms.DataFormats]::FileDrop); $f -join [Environment]::NewLine}';
188
216
  export function readClipboardImageDetailed() {
189
217
  if (process.platform === 'linux' || process.platform === 'freebsd' || process.platform === 'openbsd') {
190
218
  const wayland = process.env.WAYLAND_DISPLAY;
191
219
  const x11 = process.env.DISPLAY;
192
220
  const attempts = [];
221
+ // On WSL the Windows clipboard is what the user copies into — read it first.
222
+ if (isWsl()) {
223
+ attempts.push({ bin: 'powershell.exe', args: ['-NoProfile', '-NonInteractive', '-Command', PS_GET_IMAGE], via: 'powershell.exe' });
224
+ }
193
225
  if (wayland || !x11)
194
226
  attempts.push({ bin: 'wl-paste', args: ['--type', 'image/png', '--no-newline'], via: 'wl-paste' });
195
227
  if (x11 || !wayland) {
@@ -265,3 +297,36 @@ export async function findFileByName(workdir, name) {
265
297
  catch { }
266
298
  return null;
267
299
  }
300
+ // Read file paths from the Windows clipboard (copied files, CF_HDROP) and
301
+ // translate them to WSL paths. On WSL the user often copies a FILE, not an
302
+ // image. Returns [] on non-WSL / when nothing is there.
303
+ export function readWindowsClipboardFiles() {
304
+ if (!isWsl())
305
+ return [];
306
+ const out = tryCommand('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', PS_GET_FILE_DROP]);
307
+ if (!out)
308
+ return [];
309
+ const text = out.toString('utf-8');
310
+ return text
311
+ .split(/\r?\n/)
312
+ .map((s) => s.trim())
313
+ .filter(Boolean)
314
+ .map(winPathToWsl)
315
+ .filter((p) => !!p);
316
+ }
317
+ // Convert a Windows path (C:\\Users\\me\\a.png) into a WSL path
318
+ // (/mnt/c/Users/me/a.png). Leaves already-POSIX paths untouched.
319
+ export function winPathToWsl(winPath) {
320
+ let p = String(winPath || '').trim();
321
+ if (p.startsWith('"') && p.endsWith('"'))
322
+ p = p.slice(1, -1);
323
+ if (!p)
324
+ return null;
325
+ if (p.startsWith('/'))
326
+ return p;
327
+ if (p.charAt(1) !== ':' || (p.charAt(2) !== '/' && p.charAt(2) !== '\\'))
328
+ return null;
329
+ const drive = p.charAt(0).toLowerCase();
330
+ const rest = p.slice(3).split('\\').join('/');
331
+ return '/mnt/' + drive + '/' + rest;
332
+ }
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { createTools } from './tools.js';
8
8
  import { runAgentLoop } from './agent-loop.js';
9
9
  import { createSpinner } from './spinner.js';
10
10
  import { LineEditor, expandPastes, pasteReplacement } from './input.js';
11
- import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, } from './attachments.js';
11
+ import { parseImagePaste, extForMime, saveToTemp, guessMime, isImageName, formatSize, looksLikeFilePath, readClipboardImageDetailed, sniffMime, readWindowsClipboardFiles, } from './attachments.js';
12
12
  import { loadConfig, DEFAULTS, CONFIG_PATHS, ZAMES_HOME, CONFIG_SCHEMA, getByPath, validateConfigValue, writeConfigValue, resetConfigValue, } from './config.js';
13
13
  import { runConfigMenu } from './config-menu.js';
14
14
  import { translate, normalizeLocale, localeDisplayName, isLocale, } from './i18n.js';
@@ -997,6 +997,26 @@ async function main() {
997
997
  ed.onClipboard = async () => {
998
998
  const res = readClipboardImageDetailed();
999
999
  if (!res.data || !res.data.length) {
1000
+ // On WSL the user may have copied a FILE in Windows (not an image):
1001
+ // the Windows clipboard holds its path - attach it directly.
1002
+ for (const fp of readWindowsClipboardFiles()) {
1003
+ const data = await fs.readFile(fp).catch(() => null);
1004
+ if (!data)
1005
+ continue;
1006
+ const nm = path.basename(fp);
1007
+ const att = ed.attachments.add({
1008
+ path: fp,
1009
+ name: nm,
1010
+ mime: guessMime(nm),
1011
+ size: data.length,
1012
+ });
1013
+ ed.printAbove(theme.system(t('msg.attached_file', {
1014
+ marker: att.marker,
1015
+ name: nm,
1016
+ size: formatSize(data.length),
1017
+ }) + theme.dim(' (windows-clipboard)')));
1018
+ return att;
1019
+ }
1000
1020
  if (!clipboardWarned) {
1001
1021
  clipboardWarned = true;
1002
1022
  ed.printAbove(theme.warn(t('msg.clip_empty', { via: res.via }) +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "zames_pro",
3
- "version": "2.8.0",
3
+ "version": "2.9.0",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {