zames_pro 2.8.0 → 2.9.1

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.
188
- export function readClipboardImageDetailed() {
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 image grabber, embedded as a UTF-16LE base64 script and run
206
+ // via powershell.exe -EncodedCommand: it saves the clipboard image to a
207
+ // Windows temp .png and prints the Windows path; we read it from WSL.
208
+ const PS_IMAGE_ENCODED = 'QQBkAGQALQBUAHkAcABlACAALQBBAHMAcwBlAG0AYgBsAHkATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwAsAFMAeQBzAHQAZQBtAC4ARAByAGEAdwBpAG4AZwAKACQAaQBtAGcAPQBbAFMAeQBzAHQAZQBtAC4AVwBpAG4AZABvAHcAcwAuAEYAbwByAG0AcwAuAEMAbABpAHAAYgBvAGEAcgBkAF0AOgA6AEcAZQB0AEkAbQBhAGcAZQAoACkACgBpAGYAKAAkAGkAbQBnACkAewAkAHAAPQBKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBUAEUATQBQACAAKAAiAHoAYQBtAGUAcwAtAGMAbABpAHAALQAiACsAWwBHAHUAaQBkAF0AOgA6AE4AZQB3AEcAdQBpAGQAKAApAC4AVABvAFMAdAByAGkAbgBnACgAKQArACIALgBwAG4AZwAiACkAOwAkAGkAbQBnAC4AUwBhAHYAZQAoACQAcAAsAFsAUwB5AHMAdABlAG0ALgBEAHIAYQB3AGkAbgBnAC4ASQBtAGEAZwBpAG4AZwAuAEkAbQBhAGcAZQBGAG8AcgBtAGEAdABdADoAOgBQAG4AZwApADsAVwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIAAkAHAAfQAKAA==';
209
+ // PowerShell one-liner: write the clipboard file paths (CF_HDROP) to stdout.
210
+ const PS_GET_FILE_DROP = 'Add-Type -AssemblyName System.Windows.Forms; ' +
211
+ '$d=[System.Windows.Forms.Clipboard]::GetDataObject(); ' +
212
+ 'if($d -and $d.GetDataPresent([System.Windows.Forms.DataFormats]::FileDrop)){' +
213
+ '$f=$d.GetData([System.Windows.Forms.DataFormats]::FileDrop); $f -join [Environment]::NewLine}';
214
+ export async function readClipboardImageDetailed() {
189
215
  if (process.platform === 'linux' || process.platform === 'freebsd' || process.platform === 'openbsd') {
190
216
  const wayland = process.env.WAYLAND_DISPLAY;
191
217
  const x11 = process.env.DISPLAY;
192
218
  const attempts = [];
219
+ // On WSL the Windows clipboard is what the user copies into — read it first.
220
+ if (isWsl()) {
221
+ const win = await readWindowsClipboardImage();
222
+ if (win)
223
+ return { data: win, 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) {
@@ -229,8 +261,8 @@ export function readClipboardImageDetailed() {
229
261
  return { data: null, via: 'unsupported-platform' };
230
262
  }
231
263
  // Backwards-compatible wrapper used elsewhere.
232
- export function readClipboardImage() {
233
- return readClipboardImageDetailed().data;
264
+ export async function readClipboardImage() {
265
+ return (await readClipboardImageDetailed()).data;
234
266
  }
235
267
  function tryCommand(bin, args) {
236
268
  try {
@@ -265,3 +297,56 @@ 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', ['-STA', '-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
+ }
333
+ // Run the embedded PowerShell script and return the clipboard image bytes.
334
+ export async function readWindowsClipboardImage() {
335
+ if (!isWsl())
336
+ return null;
337
+ const out = tryCommand('powershell.exe', ['-STA', '-NoProfile', '-NonInteractive', '-EncodedCommand', PS_IMAGE_ENCODED]);
338
+ if (!out)
339
+ return null;
340
+ const first = out.toString('utf-8').trim().split(String.fromCharCode(10))[0].trim();
341
+ if (!first)
342
+ return null;
343
+ const wslPath = winPathToWsl(first);
344
+ if (!wslPath)
345
+ return null;
346
+ try {
347
+ return await fs.readFile(wslPath);
348
+ }
349
+ catch {
350
+ return null;
351
+ }
352
+ }
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';
@@ -995,8 +995,28 @@ async function main() {
995
995
  return att;
996
996
  };
997
997
  ed.onClipboard = async () => {
998
- const res = readClipboardImageDetailed();
998
+ const res = await 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.1",
4
4
  "description": "Terminal coding agent over chat.deepseek.com via Playwright",
5
5
  "type": "module",
6
6
  "bin": {