@pixelbyte-software/pixcode 1.36.1 → 1.36.3

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.
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+
3
+ import process from 'node:process';
4
+
5
+ const apiUrl = (process.env.PIXCODE_API_URL || 'http://127.0.0.1:3001').replace(/\/$/, '');
6
+ const apiKey = process.env.PIXCODE_API_KEY || process.env.PIXCODE_AGENT_API_KEY || '';
7
+ const providers = (process.env.PIXCODE_PROVIDERS || 'claude,cursor,codex,gemini,qwen,opencode')
8
+ .split(',')
9
+ .map((provider) => provider.trim())
10
+ .filter(Boolean);
11
+ const projectPath = process.env.PIXCODE_PROJECT_PATH || process.cwd();
12
+ const message = process.env.PIXCODE_SMOKE_MESSAGE
13
+ || 'Reply with exactly "pixcode-smoke-ok". Do not edit files.';
14
+ const timeoutMs = Number(process.env.PIXCODE_SMOKE_TIMEOUT_MS || 180000);
15
+ const modelMap = parseModelMap(process.env.PIXCODE_PROVIDER_MODELS || '{}');
16
+
17
+ if (!apiKey) {
18
+ console.error('PIXCODE_API_KEY is required. Create one in Settings -> API Keys and rerun.');
19
+ process.exit(2);
20
+ }
21
+
22
+ const results = [];
23
+
24
+ for (const provider of providers) {
25
+ const startedAt = Date.now();
26
+ const controller = new AbortController();
27
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
28
+
29
+ try {
30
+ const body = {
31
+ provider,
32
+ projectPath,
33
+ message,
34
+ stream: false,
35
+ cleanup: false,
36
+ };
37
+ if (modelMap[provider]) {
38
+ body.model = modelMap[provider];
39
+ }
40
+
41
+ const response = await fetch(`${apiUrl}/api/agent`, {
42
+ method: 'POST',
43
+ headers: {
44
+ Authorization: `Bearer ${apiKey}`,
45
+ 'Content-Type': 'application/json',
46
+ },
47
+ body: JSON.stringify(body),
48
+ signal: controller.signal,
49
+ });
50
+
51
+ const text = await response.text();
52
+ const payload = safeJson(text);
53
+ const assistantText = extractAssistantText(payload);
54
+ const ok = response.ok && payload?.success === true && assistantText.trim().length > 0;
55
+
56
+ results.push({
57
+ provider,
58
+ status: ok ? 'ok' : 'provider-error',
59
+ httpStatus: response.status,
60
+ success: Boolean(payload?.success),
61
+ durationMs: Date.now() - startedAt,
62
+ sessionId: payload?.sessionId || null,
63
+ assistantPreview: assistantText.trim().slice(0, 240),
64
+ error: ok ? null : (payload?.error || payload?.rawError || text.slice(0, 500)),
65
+ });
66
+ } catch (error) {
67
+ results.push({
68
+ provider,
69
+ status: error?.name === 'AbortError' ? 'timeout' : 'transport-error',
70
+ httpStatus: null,
71
+ success: false,
72
+ durationMs: Date.now() - startedAt,
73
+ sessionId: null,
74
+ assistantPreview: '',
75
+ error: error?.message || String(error),
76
+ });
77
+ } finally {
78
+ clearTimeout(timer);
79
+ }
80
+ }
81
+
82
+ console.log(JSON.stringify({ apiUrl, projectPath, results }, null, 2));
83
+ process.exit(results.every((result) => result.status === 'ok') ? 0 : 1);
84
+
85
+ function parseModelMap(raw) {
86
+ try {
87
+ const parsed = JSON.parse(raw);
88
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
89
+ } catch {
90
+ return {};
91
+ }
92
+ }
93
+
94
+ function safeJson(text) {
95
+ try {
96
+ return JSON.parse(text);
97
+ } catch {
98
+ return null;
99
+ }
100
+ }
101
+
102
+ function extractAssistantText(payload) {
103
+ if (!payload || !Array.isArray(payload.messages)) return '';
104
+
105
+ const chunks = [];
106
+ for (const messageItem of payload.messages) {
107
+ if (messageItem?.type !== 'assistant') continue;
108
+ const content = messageItem.message?.content;
109
+ if (typeof content === 'string') {
110
+ chunks.push(content);
111
+ continue;
112
+ }
113
+ if (Array.isArray(content)) {
114
+ for (const part of content) {
115
+ if (typeof part === 'string') {
116
+ chunks.push(part);
117
+ } else if (part?.type === 'text' && typeof part.text === 'string') {
118
+ chunks.push(part.text);
119
+ }
120
+ }
121
+ }
122
+ }
123
+ return chunks.join('');
124
+ }
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs';
4
+
5
+ const checks = [
6
+ {
7
+ name: 'update frequency defaults to 30 minutes',
8
+ file: 'src/utils/updateCheckPreferences.ts',
9
+ test: (source) => (
10
+ source.includes("export type UpdateCheckFrequency = 'off' | '30m' |")
11
+ && source.includes("{ value: '30m'")
12
+ && /DEFAULT_UPDATE_CHECK_PREFERENCES[\s\S]*frequency:\s*'30m'/.test(source)
13
+ ),
14
+ },
15
+ {
16
+ name: 'sidebar opens the version modal when an update is detected',
17
+ file: 'src/components/sidebar/view/Sidebar.tsx',
18
+ test: (source) => (
19
+ source.includes('PIXCODE_UPDATE_AVAILABLE_EVENT')
20
+ && source.includes('setShowVersionModal(true)')
21
+ && source.includes('updateAvailable')
22
+ ),
23
+ },
24
+ {
25
+ name: 'version modal supports release-notes-only opens',
26
+ file: 'src/components/version-upgrade/view/VersionUpgradeModal.tsx',
27
+ test: (source) => (
28
+ source.includes('isUpdateAvailable')
29
+ && source.includes('versionUpdate.releaseNotesTitle')
30
+ && source.includes('showUpdateActions')
31
+ ),
32
+ },
33
+ {
34
+ name: 'desktop splash makes startup update work visible',
35
+ file: 'desktop/electron/main.cjs',
36
+ test: (source) => (
37
+ source.includes('Checking for updates before launch')
38
+ && source.includes('Applying Pixcode update')
39
+ ),
40
+ },
41
+ ];
42
+
43
+ const failures = [];
44
+
45
+ for (const check of checks) {
46
+ const source = readFileSync(check.file, 'utf8');
47
+ if (!check.test(source)) failures.push(check.name);
48
+ }
49
+
50
+ if (failures.length > 0) {
51
+ console.error(`Update UX smoke failed:\n- ${failures.join('\n- ')}`);
52
+ process.exit(1);
53
+ }
54
+
55
+ console.log('update UX smoke passed');
@@ -193,16 +193,55 @@ export class OpencodeSessionsProvider implements IProviderSessions {
193
193
  }
194
194
 
195
195
  const normalized: NormalizedMessage[] = [];
196
- for (const raw of rawMessages) {
196
+ for (let i = 0; i < rawMessages.length; i++) {
197
+ const raw = rawMessages[i];
197
198
  const ts = raw.timestamp || new Date().toISOString();
198
- const baseId = raw.uuid || raw.id || generateMessageId('opencode');
199
+ const baseId = raw.uuid || raw.id || `${PROVIDER}_${sessionId}_${i}`;
199
200
  const role = raw.message?.role || raw.role;
200
201
  const content = raw.message?.content || raw.content;
201
202
 
202
203
  if (!role || !content) continue;
203
204
  const normalizedRole = role === 'user' ? 'user' : 'assistant';
204
205
 
205
- if (typeof content === 'string' && content.trim()) {
206
+ if (Array.isArray(content)) {
207
+ for (let partIdx = 0; partIdx < content.length; partIdx++) {
208
+ const part = readObjectRecord(content[partIdx]);
209
+ if (!part) continue;
210
+ if (part.type === 'text' && typeof part.text === 'string' && part.text.trim()) {
211
+ normalized.push(createNormalizedMessage({
212
+ id: `${baseId}_${partIdx}`,
213
+ sessionId,
214
+ timestamp: ts,
215
+ provider: PROVIDER,
216
+ kind: 'text',
217
+ role: normalizedRole,
218
+ content: part.text,
219
+ }));
220
+ } else if (part.type === 'tool_use') {
221
+ normalized.push(createNormalizedMessage({
222
+ id: `${baseId}_${partIdx}`,
223
+ sessionId,
224
+ timestamp: ts,
225
+ provider: PROVIDER,
226
+ kind: 'tool_use',
227
+ toolName: String(part.name || ''),
228
+ toolInput: part.input || {},
229
+ toolId: String(part.id || `${baseId}_${partIdx}`),
230
+ }));
231
+ } else if (part.type === 'tool_result') {
232
+ normalized.push(createNormalizedMessage({
233
+ id: `${baseId}_${partIdx}`,
234
+ sessionId,
235
+ timestamp: ts,
236
+ provider: PROVIDER,
237
+ kind: 'tool_result',
238
+ toolId: String(part.tool_use_id || ''),
239
+ content: part.content === undefined || part.content === null ? '' : String(part.content),
240
+ isError: Boolean(part.is_error),
241
+ }));
242
+ }
243
+ }
244
+ } else if (typeof content === 'string' && content.trim()) {
206
245
  normalized.push(createNormalizedMessage({
207
246
  id: baseId,
208
247
  sessionId,
@@ -215,6 +254,21 @@ export class OpencodeSessionsProvider implements IProviderSessions {
215
254
  }
216
255
  }
217
256
 
257
+ const toolResultMap = new Map<string, NormalizedMessage>();
258
+ for (const msg of normalized) {
259
+ if (msg.kind === 'tool_result' && msg.toolId) {
260
+ toolResultMap.set(msg.toolId, msg);
261
+ }
262
+ }
263
+ for (const msg of normalized) {
264
+ if (msg.kind === 'tool_use' && msg.toolId && toolResultMap.has(msg.toolId)) {
265
+ const toolResult = toolResultMap.get(msg.toolId);
266
+ if (toolResult) {
267
+ msg.toolResult = { content: toolResult.content, isError: toolResult.isError };
268
+ }
269
+ }
270
+ }
271
+
218
272
  const start = Math.max(0, offset);
219
273
  const pageLimit = limit === null ? null : Math.max(0, limit);
220
274
  const messages = pageLimit === null
@@ -184,6 +184,20 @@ async function spawnOpencode(command, options = {}, ws) {
184
184
 
185
185
  let terminalNotificationSent = false;
186
186
  let terminalFailureReason = null;
187
+ let terminalFailurePersisted = false;
188
+
189
+ const persistTerminalFailure = (message) => {
190
+ const content = typeof message === 'string' && message.trim()
191
+ ? message.trim()
192
+ : null;
193
+ if (!content || terminalFailurePersisted) return;
194
+
195
+ const finalSessionId = capturedSessionId || sessionId || processKey;
196
+ if (!finalSessionId) return;
197
+
198
+ sessionManager.addMessage(finalSessionId, 'assistant', content);
199
+ terminalFailurePersisted = true;
200
+ };
187
201
 
188
202
  const notifyTerminalState = ({ code = null, error = null } = {}) => {
189
203
  if (terminalNotificationSent) return;
@@ -277,6 +291,9 @@ async function spawnOpencode(command, options = {}, ws) {
277
291
  }
278
292
  }
279
293
  },
294
+ onError: (content) => {
295
+ terminalFailureReason = content || 'OpenCode streaming error';
296
+ },
280
297
  });
281
298
  }
282
299
 
@@ -338,6 +355,7 @@ async function spawnOpencode(command, options = {}, ws) {
338
355
  friendly = 'OpenCode upstream call failed. Clearing `~/.cache/opencode` and retrying usually resolves this.';
339
356
  }
340
357
 
358
+ terminalFailureReason = friendly;
341
359
  const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : (capturedSessionId || sessionId);
342
360
  ws.send(createNormalizedMessage({ kind: 'error', content: friendly, sessionId: socketSessionId, provider: 'opencode' }));
343
361
  });
@@ -355,6 +373,8 @@ async function spawnOpencode(command, options = {}, ws) {
355
373
 
356
374
  if (finalSessionId && assistantBlocks.length > 0) {
357
375
  sessionManager.addMessage(finalSessionId, 'assistant', assistantBlocks);
376
+ } else if (terminalFailureReason) {
377
+ persistTerminalFailure(terminalFailureReason);
358
378
  }
359
379
 
360
380
  ws.send(createNormalizedMessage({ kind: 'complete', exitCode: code, isNewSession: !sessionId && !!command, sessionId: finalSessionId, provider: 'opencode' }));
@@ -376,9 +396,11 @@ async function spawnOpencode(command, options = {}, ws) {
376
396
  const installed = await providerAuthService.isProviderInstalled('opencode');
377
397
  if (!installed) {
378
398
  const socketSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
399
+ const installMessage = 'OpenCode CLI is not installed. Install it from the Settings → Agents → OpenCode tab, or run: npm install -g opencode-ai';
400
+ terminalFailureReason = installMessage;
379
401
  ws.send(createNormalizedMessage({
380
402
  kind: 'error',
381
- content: 'OpenCode CLI is not installed. Install it from the Settings → Agents → OpenCode tab, or run: npm install -g opencode-ai',
403
+ content: installMessage,
382
404
  sessionId: socketSessionId,
383
405
  provider: 'opencode',
384
406
  }));
@@ -404,6 +426,7 @@ async function spawnOpencode(command, options = {}, ws) {
404
426
 
405
427
  const errorSessionId = typeof ws.getSessionId === 'function' ? ws.getSessionId() : finalSessionId;
406
428
  ws.send(createNormalizedMessage({ kind: 'error', content: errorContent, sessionId: errorSessionId, provider: 'opencode' }));
429
+ persistTerminalFailure(errorContent);
407
430
  // Always emit `complete` so the UI's "Processing..." state clears
408
431
  // even when spawn fails (ENOENT, EACCES) and `close` never fires.
409
432
  ws.send(createNormalizedMessage({ kind: 'complete', exitCode: 1, isNewSession: !sessionId && !!command, sessionId: errorSessionId, provider: 'opencode' }));
@@ -27,6 +27,7 @@ class OpencodeResponseHandler {
27
27
  this.onInit = options.onInit || null;
28
28
  this.onToolUse = options.onToolUse || null;
29
29
  this.onToolResult = options.onToolResult || null;
30
+ this.onError = options.onError || null;
30
31
  this.capturedCliSessionId = null;
31
32
  }
32
33
 
@@ -84,6 +85,9 @@ class OpencodeResponseHandler {
84
85
 
85
86
  const normalized = sessionsService.normalizeMessage('opencode', event, sid);
86
87
  for (const msg of normalized) {
88
+ if (msg.kind === 'error' && this.onError) {
89
+ this.onError(msg.content || 'OpenCode streaming error');
90
+ }
87
91
  this.ws.send(msg);
88
92
  }
89
93
  }
@@ -268,7 +268,7 @@ function describeProviderFailure(rawError, provider) {
268
268
  details.category = 'auth';
269
269
  details.title = `${name} is not authenticated or the selected model is not allowed.`;
270
270
  details.action = 'Reconnect this provider in Settings, refresh the CLI login, or choose a model enabled for the account.';
271
- } else if (/(not installed|command not found|enoent|spawn .* enoent|executable file not found)/i.test(rawMessage)) {
271
+ } else if (/(not installed|command not found|enoent|spawn .* enoent|executable file not found|exited with code 127)/i.test(rawMessage)) {
272
272
  details.category = 'missing_cli';
273
273
  details.title = `${name} CLI is not installed or not on PATH.`;
274
274
  details.action = 'Install the CLI from Settings -> Agents or set the matching CLI path environment variable.';
@@ -1411,8 +1411,11 @@ router.post('/', validateExternalApiKey, async (req, res) => {
1411
1411
  if (writer && typeof writer.getAssistantMessages === 'function') {
1412
1412
  try {
1413
1413
  collectedMessages = writer.getAssistantMessages();
1414
- const errEntry = collectedMessages.find((m) => m.type === 'error');
1415
- if (errEntry) collectedError = errEntry.content;
1414
+ const errorText = collectedMessages
1415
+ .filter((m) => m.type === 'error' && typeof m.content === 'string' && m.content.trim())
1416
+ .map((m) => m.content.trim())
1417
+ .join('\n');
1418
+ if (errorText) collectedError = errorText;
1416
1419
  } catch { /* ignore — fall back to error.message */ }
1417
1420
  }
1418
1421
  const failureDetails = describeProviderFailure(collectedError || error.message, provider);