@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.
- package/dist/assets/{index-D3aq3WpJ.js → index-Bp8mXdQd.js} +182 -182
- package/dist/index.html +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +60 -3
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
- package/dist-server/server/opencode-cli.js +24 -1
- package/dist-server/server/opencode-cli.js.map +1 -1
- package/dist-server/server/opencode-response-handler.js +4 -0
- package/dist-server/server/opencode-response-handler.js.map +1 -1
- package/dist-server/server/routes/agent.js +7 -4
- package/dist-server/server/routes/agent.js.map +1 -1
- package/package.json +4 -1
- package/scripts/smoke/chat-session-state.mjs +19 -0
- package/scripts/smoke/provider-rest-api.mjs +124 -0
- package/scripts/smoke/update-ux.mjs +55 -0
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +57 -3
- package/server/opencode-cli.js +24 -1
- package/server/opencode-response-handler.js +4 -0
- package/server/routes/agent.js +6 -3
|
@@ -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 (
|
|
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 ||
|
|
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 (
|
|
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
|
package/server/opencode-cli.js
CHANGED
|
@@ -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:
|
|
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
|
}
|
package/server/routes/agent.js
CHANGED
|
@@ -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
|
|
1415
|
-
|
|
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);
|