@pixelbyte-software/pixcode 1.33.9 → 1.33.11
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/api-docs.html +373 -857
- package/dist/assets/{index-DpIcI9Q1.js → index-oLYHJ2X5.js} +154 -166
- package/dist/index.html +1 -1
- package/dist/openapi.yaml +1311 -0
- package/dist-server/server/gemini-cli.js +59 -0
- package/dist-server/server/gemini-cli.js.map +1 -1
- package/dist-server/server/index.js +6 -1
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/middleware/auth.js +51 -9
- package/dist-server/server/middleware/auth.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js +54 -15
- package/dist-server/server/modules/providers/list/opencode/opencode-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js +46 -0
- package/dist-server/server/modules/providers/list/qwen/qwen-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +32 -1
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/opencode-cli.js +41 -2
- package/dist-server/server/opencode-cli.js.map +1 -1
- package/dist-server/server/opencode-response-handler.js +36 -34
- package/dist-server/server/opencode-response-handler.js.map +1 -1
- package/dist-server/server/routes/agent.js +187 -56
- package/dist-server/server/routes/agent.js.map +1 -1
- package/dist-server/server/routes/projects.js +134 -8
- package/dist-server/server/routes/projects.js.map +1 -1
- package/dist-server/server/services/provider-credentials.js +42 -8
- package/dist-server/server/services/provider-credentials.js.map +1 -1
- package/package.json +1 -1
- package/server/gemini-cli.js +60 -0
- package/server/index.js +6 -1
- package/server/middleware/auth.js +50 -9
- package/server/modules/providers/list/opencode/opencode-sessions.provider.ts +60 -21
- package/server/modules/providers/list/qwen/qwen-sessions.provider.ts +47 -0
- package/server/modules/providers/provider.routes.ts +37 -4
- package/server/opencode-cli.js +41 -2
- package/server/opencode-response-handler.js +36 -29
- package/server/routes/agent.js +178 -58
- package/server/routes/projects.js +136 -8
- package/server/services/provider-credentials.js +42 -8
|
@@ -16,8 +16,15 @@ const PROVIDER = 'opencode';
|
|
|
16
16
|
* captured streams); restoring historical sessions from disk will land
|
|
17
17
|
* in a follow-up once the exact schema is pinned.
|
|
18
18
|
*
|
|
19
|
-
* Stream
|
|
20
|
-
*
|
|
19
|
+
* Stream shape from `opencode run --format json` (verified against
|
|
20
|
+
* opencode-ai 0.x at the wire):
|
|
21
|
+
* { type:"step_start", timestamp, sessionID, part:{ type:"step-start", ... } }
|
|
22
|
+
* { type:"text", timestamp, sessionID, part:{ type:"text", text:"…", time:{…}, metadata:{…} } }
|
|
23
|
+
* { type:"tool_use", timestamp, sessionID, part:{ type:"tool-use", callID, tool, state:{ input } } }
|
|
24
|
+
* { type:"tool_result", timestamp, sessionID, part:{ type:"tool-result", callID, state:{ output, status } } }
|
|
25
|
+
* { type:"step_finish", timestamp, sessionID, part:{ type:"step-finish", reason, tokens, cost } }
|
|
26
|
+
* { type:"error", timestamp, sessionID, error:{ name, data:{ message, statusCode? } } }
|
|
27
|
+
* Field names are camelCase (`sessionID`, `callID`) — NOT snake_case.
|
|
21
28
|
*/
|
|
22
29
|
export class OpencodeSessionsProvider implements IProviderSessions {
|
|
23
30
|
normalizeMessage(rawMessage: unknown, sessionId: string | null): NormalizedMessage[] {
|
|
@@ -26,20 +33,39 @@ export class OpencodeSessionsProvider implements IProviderSessions {
|
|
|
26
33
|
|
|
27
34
|
const ts = raw.timestamp || new Date().toISOString();
|
|
28
35
|
const baseId = raw.uuid || raw.id || generateMessageId('opencode');
|
|
36
|
+
const part = readObjectRecord(raw.part) || {};
|
|
37
|
+
|
|
38
|
+
// Modern shape: { type:"text", part:{ type:"text", text:"…" } }.
|
|
39
|
+
// The full text arrives in a single event (not token-by-token), but it
|
|
40
|
+
// can also legitimately be empty during a step_start preamble — we only
|
|
41
|
+
// emit when there's actual text. `stream_end` is emitted on step_finish
|
|
42
|
+
// (which is guaranteed at the end of every assistant turn), so we don't
|
|
43
|
+
// close the stream here.
|
|
44
|
+
if (raw.type === 'text') {
|
|
45
|
+
const text = typeof part.text === 'string' ? part.text : '';
|
|
46
|
+
if (!text) return [];
|
|
47
|
+
return [createNormalizedMessage({
|
|
48
|
+
id: baseId,
|
|
49
|
+
sessionId,
|
|
50
|
+
timestamp: ts,
|
|
51
|
+
provider: PROVIDER,
|
|
52
|
+
kind: 'stream_delta',
|
|
53
|
+
content: text,
|
|
54
|
+
})];
|
|
55
|
+
}
|
|
29
56
|
|
|
57
|
+
// Legacy fallback for older builds that still emit `{ type:"message", role:"assistant", content }`.
|
|
30
58
|
if (raw.type === 'message' && raw.role === 'assistant') {
|
|
31
|
-
const content = raw.content
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
}));
|
|
42
|
-
}
|
|
59
|
+
const content = typeof raw.content === 'string' ? raw.content : '';
|
|
60
|
+
if (!content) return [];
|
|
61
|
+
const messages: NormalizedMessage[] = [createNormalizedMessage({
|
|
62
|
+
id: baseId,
|
|
63
|
+
sessionId,
|
|
64
|
+
timestamp: ts,
|
|
65
|
+
provider: PROVIDER,
|
|
66
|
+
kind: 'stream_delta',
|
|
67
|
+
content,
|
|
68
|
+
})];
|
|
43
69
|
if (raw.delta !== true) {
|
|
44
70
|
messages.push(createNormalizedMessage({
|
|
45
71
|
sessionId,
|
|
@@ -52,32 +78,39 @@ export class OpencodeSessionsProvider implements IProviderSessions {
|
|
|
52
78
|
}
|
|
53
79
|
|
|
54
80
|
if (raw.type === 'tool_use' || raw.type === 'tool-use') {
|
|
81
|
+
const state = readObjectRecord(part.state) || {};
|
|
55
82
|
return [createNormalizedMessage({
|
|
56
83
|
id: baseId,
|
|
57
84
|
sessionId,
|
|
58
85
|
timestamp: ts,
|
|
59
86
|
provider: PROVIDER,
|
|
60
87
|
kind: 'tool_use',
|
|
61
|
-
toolName: raw.tool_name || raw.name,
|
|
62
|
-
toolInput: raw.parameters || raw.input || {},
|
|
63
|
-
toolId: raw.tool_id || raw.id || baseId,
|
|
88
|
+
toolName: String(part.tool || raw.tool_name || raw.name || ''),
|
|
89
|
+
toolInput: state.input || part.input || raw.parameters || raw.input || {},
|
|
90
|
+
toolId: String(part.callID || part.id || raw.tool_id || raw.id || baseId),
|
|
64
91
|
})];
|
|
65
92
|
}
|
|
66
93
|
|
|
67
94
|
if (raw.type === 'tool_result' || raw.type === 'tool-result') {
|
|
95
|
+
const state = readObjectRecord(part.state) || {};
|
|
96
|
+
const status = (state.status || raw.status) as string | undefined;
|
|
97
|
+
const output = state.output ?? part.output ?? raw.output;
|
|
68
98
|
return [createNormalizedMessage({
|
|
69
99
|
id: baseId,
|
|
70
100
|
sessionId,
|
|
71
101
|
timestamp: ts,
|
|
72
102
|
provider: PROVIDER,
|
|
73
103
|
kind: 'tool_result',
|
|
74
|
-
toolId: raw.tool_id || raw.toolCallId || '',
|
|
75
|
-
content:
|
|
76
|
-
isError:
|
|
104
|
+
toolId: String(part.callID || part.id || raw.tool_id || raw.toolCallId || ''),
|
|
105
|
+
content: output === undefined || output === null ? '' : String(output),
|
|
106
|
+
isError: status === 'error' || Boolean(raw.isError),
|
|
77
107
|
})];
|
|
78
108
|
}
|
|
79
109
|
|
|
80
|
-
|
|
110
|
+
// OpenCode signals end-of-turn with `step_finish` (run mode never emits
|
|
111
|
+
// a top-level `result`). Emit stream_end so the UI clears its
|
|
112
|
+
// "Processing…" state.
|
|
113
|
+
if (raw.type === 'step_finish' || raw.type === 'result') {
|
|
81
114
|
return [createNormalizedMessage({
|
|
82
115
|
sessionId,
|
|
83
116
|
timestamp: ts,
|
|
@@ -86,6 +119,12 @@ export class OpencodeSessionsProvider implements IProviderSessions {
|
|
|
86
119
|
})];
|
|
87
120
|
}
|
|
88
121
|
|
|
122
|
+
// step_start carries the session ID but no user-visible content — we
|
|
123
|
+
// capture the session ID elsewhere (response handler) and drop the event.
|
|
124
|
+
if (raw.type === 'step_start') {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
89
128
|
if (raw.type === 'error') {
|
|
90
129
|
// OpenCode `--format json` emits errors as
|
|
91
130
|
// { type:"error", error:{ name, data:{ message, statusCode?, isRetryable? } } }
|
|
@@ -20,6 +20,53 @@ export class QwenSessionsProvider implements IProviderSessions {
|
|
|
20
20
|
const ts = raw.timestamp || new Date().toISOString();
|
|
21
21
|
const baseId = raw.uuid || generateMessageId('qwen');
|
|
22
22
|
|
|
23
|
+
// Qwen Code stream-json (verified from `qwen --output-format stream-json --yolo`):
|
|
24
|
+
// { type:"system", subtype:"init", session_id, ... }
|
|
25
|
+
// { type:"assistant", uuid, session_id, message:{ role:"assistant", content:[{type:"text", text:"…"}], usage:{...} } }
|
|
26
|
+
// { type:"result", subtype:"success", result:"…final…", usage:{...} }
|
|
27
|
+
// The previous matcher (`type==='message' && role==='assistant'`) checked
|
|
28
|
+
// a shape Qwen has never emitted — every chat returned empty messages,
|
|
29
|
+
// including API-error messages like "[API Error: 401 invalid access token
|
|
30
|
+
// or token expired]" which then landed in the void instead of reaching
|
|
31
|
+
// the user. Match the real shape.
|
|
32
|
+
if (raw.type === 'assistant' && raw.message && typeof raw.message === 'object') {
|
|
33
|
+
const msg = raw.message as Record<string, unknown>;
|
|
34
|
+
const parts = Array.isArray(msg.content) ? (msg.content as Record<string, unknown>[]) : [];
|
|
35
|
+
const text = parts
|
|
36
|
+
.map((p) => (typeof p?.text === 'string' ? p.text : ''))
|
|
37
|
+
.filter(Boolean)
|
|
38
|
+
.join('');
|
|
39
|
+
const messages: NormalizedMessage[] = [];
|
|
40
|
+
if (text) {
|
|
41
|
+
messages.push(createNormalizedMessage({
|
|
42
|
+
id: baseId,
|
|
43
|
+
sessionId,
|
|
44
|
+
timestamp: ts,
|
|
45
|
+
provider: PROVIDER,
|
|
46
|
+
kind: 'stream_delta',
|
|
47
|
+
content: text,
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
// Surface tool_use parts inline so the agent route can see them too.
|
|
51
|
+
for (const part of parts) {
|
|
52
|
+
if (part?.type === 'tool_use') {
|
|
53
|
+
messages.push(createNormalizedMessage({
|
|
54
|
+
id: typeof part.id === 'string' ? part.id : generateMessageId('qwen'),
|
|
55
|
+
sessionId,
|
|
56
|
+
timestamp: ts,
|
|
57
|
+
provider: PROVIDER,
|
|
58
|
+
kind: 'tool_use',
|
|
59
|
+
toolName: typeof part.name === 'string' ? part.name : '',
|
|
60
|
+
toolInput: (part.input as AnyRecord) || {},
|
|
61
|
+
toolId: typeof part.id === 'string' ? part.id : '',
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return messages;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Legacy `type:"message"` shape — kept so any persisted history that
|
|
69
|
+
// pre-dates the stream-json migration still parses cleanly.
|
|
23
70
|
if (raw.type === 'message' && raw.role === 'assistant') {
|
|
24
71
|
const content = raw.content || '';
|
|
25
72
|
const messages: NormalizedMessage[] = [];
|
|
@@ -85,6 +85,31 @@ const PROVIDER_INSTALL_COMMANDS: Record<LLMProvider, string | null> = {
|
|
|
85
85
|
cursor: null,
|
|
86
86
|
};
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Per-provider manual install hints, surfaced when `/install` is called
|
|
90
|
+
* for a provider Pixcode can't sandbox-install (anything not on npm).
|
|
91
|
+
* Each entry includes platform-specific commands so the UI can show the
|
|
92
|
+
* right one for the user's host. Cursor is the only provider in this
|
|
93
|
+
* bucket today — it ships via curl|bash on POSIX and a downloadable
|
|
94
|
+
* installer on Windows. We deliberately don't pipe-to-bash from Pixcode,
|
|
95
|
+
* so the user runs it themselves.
|
|
96
|
+
*/
|
|
97
|
+
const PROVIDER_MANUAL_INSTALL: Partial<Record<LLMProvider, {
|
|
98
|
+
docsUrl: string;
|
|
99
|
+
steps: { platform: 'macos' | 'linux' | 'windows'; command: string }[];
|
|
100
|
+
note: string;
|
|
101
|
+
}>> = {
|
|
102
|
+
cursor: {
|
|
103
|
+
docsUrl: 'https://docs.cursor.com/en/cli/installation',
|
|
104
|
+
steps: [
|
|
105
|
+
{ platform: 'macos', command: 'curl https://cursor.com/install -fsS | bash' },
|
|
106
|
+
{ platform: 'linux', command: 'curl https://cursor.com/install -fsS | bash' },
|
|
107
|
+
{ platform: 'windows', command: 'iwr https://cursor.com/install.ps1 -useb | iex' },
|
|
108
|
+
],
|
|
109
|
+
note: 'Cursor ships outside npm — run the command for your platform in a separate terminal, then click "Refresh" on this page once the binary is on PATH.',
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
|
|
88
113
|
const router = express.Router();
|
|
89
114
|
|
|
90
115
|
const readPathParam = (value: unknown, name: string): string => {
|
|
@@ -450,10 +475,18 @@ router.post(
|
|
|
450
475
|
const packageName = PROVIDER_INSTALL_PACKAGES[parsed];
|
|
451
476
|
const installCmd = PROVIDER_INSTALL_COMMANDS[parsed];
|
|
452
477
|
if (!packageName || !installCmd) {
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
478
|
+
const manual = PROVIDER_MANUAL_INSTALL[parsed];
|
|
479
|
+
// Don't 4xx on this — the call ISN'T malformed, the provider is
|
|
480
|
+
// simply not npm-installable. Return 200 with a `manual` payload
|
|
481
|
+
// the UI can present as instructions instead of "install failed".
|
|
482
|
+
res.json(createApiSuccessResponse({
|
|
483
|
+
provider: parsed,
|
|
484
|
+
manual: manual || null,
|
|
485
|
+
message: manual
|
|
486
|
+
? `${parsed} ships outside npm. Run the platform-specific command, then refresh.`
|
|
487
|
+
: `${parsed} cannot be installed automatically — see the provider's documentation.`,
|
|
488
|
+
}));
|
|
489
|
+
return;
|
|
457
490
|
}
|
|
458
491
|
|
|
459
492
|
const job = createInstallJob({ provider: parsed, installCmd, packageName });
|
package/server/opencode-cli.js
CHANGED
|
@@ -50,13 +50,34 @@ function mapPermissionModeToArgs(permissionMode, skipPermissions) {
|
|
|
50
50
|
return { agent: 'build', dangerously: false };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// Pixcode stores OpenCode permissions as two flat lists (`allowPatterns`,
|
|
54
|
+
// `denyPatterns`) in `opencode-settings` localStorage; the backend collapses
|
|
55
|
+
// them into the JSON shape OpenCode reads from `opencode.json`. We pass the
|
|
56
|
+
// merged config inline via `OPENCODE_CONFIG_CONTENT` (a documented OpenCode
|
|
57
|
+
// env var) instead of writing to disk — that way Pixcode's intent doesn't
|
|
58
|
+
// pollute the user's project tree, and project-local opencode.json overrides
|
|
59
|
+
// still take precedence per OpenCode's own precedence rules.
|
|
60
|
+
function buildOpencodePermissionConfig(settings) {
|
|
61
|
+
const allow = Array.isArray(settings?.allowPatterns) ? settings.allowPatterns.filter(Boolean) : [];
|
|
62
|
+
const deny = Array.isArray(settings?.denyPatterns) ? settings.denyPatterns.filter(Boolean) : [];
|
|
63
|
+
if (!allow.length && !deny.length) return null;
|
|
64
|
+
|
|
65
|
+
const bash = {};
|
|
66
|
+
// Deny rules win when patterns collide — apply allow first so deny
|
|
67
|
+
// overwrites duplicates.
|
|
68
|
+
for (const pattern of allow) bash[pattern] = 'allow';
|
|
69
|
+
for (const pattern of deny) bash[pattern] = 'deny';
|
|
70
|
+
|
|
71
|
+
return { permission: { bash } };
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
async function spawnOpencode(command, options = {}, ws) {
|
|
54
75
|
const { sessionId, projectPath, cwd, toolsSettings, permissionMode, images, sessionSummary, agent: agentOverride } = options;
|
|
55
76
|
let capturedSessionId = sessionId;
|
|
56
77
|
let sessionCreatedSent = false;
|
|
57
78
|
let assistantBlocks = [];
|
|
58
79
|
|
|
59
|
-
const settings = toolsSettings || {
|
|
80
|
+
const settings = toolsSettings || { allowPatterns: [], denyPatterns: [], skipPermissions: false };
|
|
60
81
|
|
|
61
82
|
const safeSessionIdPattern = /^[a-zA-Z0-9_.\-:]+$/;
|
|
62
83
|
const args = ['run', '--format', 'json'];
|
|
@@ -73,7 +94,10 @@ async function spawnOpencode(command, options = {}, ws) {
|
|
|
73
94
|
if (sessionId) {
|
|
74
95
|
const session = sessionManager.getSession(sessionId);
|
|
75
96
|
const cliId = session?.cliSessionId || sessionId;
|
|
76
|
-
|
|
97
|
+
// OpenCode CLI requires session IDs to start with 'ses' (e.g. ses_22d6…).
|
|
98
|
+
// Pixcode's internal IDs (opencode_xxxxx) must NOT be passed to -s,
|
|
99
|
+
// otherwise the CLI exits with "Invalid session ID: must start with 'ses'".
|
|
100
|
+
if (cliId && safeSessionIdPattern.test(cliId) && cliId.startsWith('ses')) {
|
|
77
101
|
args.push('-s', cliId);
|
|
78
102
|
}
|
|
79
103
|
}
|
|
@@ -135,6 +159,21 @@ async function spawnOpencode(command, options = {}, ws) {
|
|
|
135
159
|
|
|
136
160
|
const spawnEnv = await buildSpawnEnv('opencode');
|
|
137
161
|
|
|
162
|
+
// Inject Pixcode's permission allow/deny patterns as inline OpenCode config.
|
|
163
|
+
// Skipped when `--dangerously-skip-permissions` is on (the flag overrides
|
|
164
|
+
// the config anyway) and when both lists are empty (avoid clobbering a
|
|
165
|
+
// project-local opencode.json the user has hand-tuned).
|
|
166
|
+
if (!dangerously) {
|
|
167
|
+
const permConfig = buildOpencodePermissionConfig(settings);
|
|
168
|
+
if (permConfig) {
|
|
169
|
+
try {
|
|
170
|
+
spawnEnv.OPENCODE_CONFIG_CONTENT = JSON.stringify(permConfig);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.warn('[opencode] Failed to serialize permission config:', error?.message || error);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
138
177
|
return new Promise((resolve, reject) => {
|
|
139
178
|
const opencodeProcess = spawnFunction(spawnCmd, spawnArgs, {
|
|
140
179
|
cwd: workingDir,
|
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
// OpenCode Response Handler — `opencode run --format json` parser.
|
|
2
2
|
//
|
|
3
|
-
// The JSON format streams one event per line (NDJSON).
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// The JSON format streams one event per line (NDJSON). Verified shapes
|
|
4
|
+
// (opencode-ai 0.x, see opencode.ai/docs/cli):
|
|
5
|
+
//
|
|
6
|
+
// { type:"step_start", timestamp, sessionID, part:{ type:"step-start", id, messageID, sessionID } }
|
|
7
|
+
// { type:"text", timestamp, sessionID, part:{ type:"text", id, messageID, sessionID, text:"…", time:{…}, metadata:{…} } }
|
|
8
|
+
// { type:"tool_use", timestamp, sessionID, part:{ type:"tool-use", callID, tool, state:{ input } } }
|
|
9
|
+
// { type:"tool_result", timestamp, sessionID, part:{ type:"tool-result", callID, state:{ output, status } } }
|
|
10
|
+
// { type:"step_finish", timestamp, sessionID, part:{ type:"step-finish", reason, tokens, cost } }
|
|
11
|
+
// { type:"error", timestamp, sessionID, error:{ name, data:{ message, statusCode? } } }
|
|
12
|
+
//
|
|
13
|
+
// Important: field names are camelCase — `sessionID`, `callID`, `messageID`.
|
|
14
|
+
// Earlier integration code assumed snake_case (`session_id`, `tool_id`) which
|
|
15
|
+
// is wrong; nothing in `opencode run --format json` uses snake_case.
|
|
16
|
+
//
|
|
17
|
+
// Lines that don't parse as JSON are surfaced as plain text deltas (covers
|
|
18
|
+
// the CLI's pre-stream banner output, "Shell cwd was reset to …" notices on
|
|
19
|
+
// Windows, and any debug noise).
|
|
8
20
|
import { sessionsService } from './modules/providers/services/sessions.service.js';
|
|
9
21
|
|
|
10
22
|
class OpencodeResponseHandler {
|
|
@@ -15,6 +27,7 @@ class OpencodeResponseHandler {
|
|
|
15
27
|
this.onInit = options.onInit || null;
|
|
16
28
|
this.onToolUse = options.onToolUse || null;
|
|
17
29
|
this.onToolResult = options.onToolResult || null;
|
|
30
|
+
this.capturedCliSessionId = null;
|
|
18
31
|
}
|
|
19
32
|
|
|
20
33
|
processData(data) {
|
|
@@ -40,37 +53,31 @@ class OpencodeResponseHandler {
|
|
|
40
53
|
handleEvent(event) {
|
|
41
54
|
const sid = typeof this.ws.getSessionId === 'function' ? this.ws.getSessionId() : null;
|
|
42
55
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
// Capture OpenCode's real session ID (e.g. `ses_22d6…`) the first time we
|
|
57
|
+
// see one. Every event carries `sessionID` at the top level — we don't
|
|
58
|
+
// wait for an `init`/`session.start` event because OpenCode `run --format
|
|
59
|
+
// json` never emits those (the first event is always `step_start`).
|
|
60
|
+
if (!this.capturedCliSessionId && typeof event.sessionID === 'string' && event.sessionID) {
|
|
61
|
+
this.capturedCliSessionId = event.sessionID;
|
|
62
|
+
if (this.onInit) this.onInit({ session_id: event.sessionID, sessionID: event.sessionID });
|
|
46
63
|
}
|
|
47
64
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (event.type === 'text' && event.part && typeof event.part.text === 'string') {
|
|
53
|
-
if (this.onContentFragment && event.part.text) this.onContentFragment(event.part.text);
|
|
54
|
-
} else if (event.type === 'message' && event.role === 'assistant') {
|
|
55
|
-
const content = event.content || event.text || '';
|
|
56
|
-
if (this.onContentFragment && content) this.onContentFragment(content);
|
|
57
|
-
} else if (event.type === 'part' && event.part_type === 'text') {
|
|
58
|
-
const content = event.text || event.content || '';
|
|
59
|
-
if (this.onContentFragment && content) this.onContentFragment(content);
|
|
65
|
+
const part = event.part && typeof event.part === 'object' ? event.part : null;
|
|
66
|
+
|
|
67
|
+
if (event.type === 'text' && part && typeof part.text === 'string') {
|
|
68
|
+
if (this.onContentFragment && part.text) this.onContentFragment(part.text);
|
|
60
69
|
} else if (event.type === 'tool_use' || event.type === 'tool-use' || event.type === 'tool.start') {
|
|
61
|
-
|
|
62
|
-
const part = event.part || event;
|
|
70
|
+
const state = part?.state && typeof part.state === 'object' ? part.state : {};
|
|
63
71
|
if (this.onToolUse) this.onToolUse({
|
|
64
|
-
tool_id: part
|
|
65
|
-
tool_name: part
|
|
66
|
-
parameters:
|
|
72
|
+
tool_id: part?.callID || part?.id || event.tool_id || '',
|
|
73
|
+
tool_name: part?.tool || part?.name || event.tool_name || '',
|
|
74
|
+
parameters: state.input || part?.input || event.parameters || {},
|
|
67
75
|
});
|
|
68
76
|
} else if (event.type === 'tool_result' || event.type === 'tool-result' || event.type === 'tool.end') {
|
|
69
|
-
const
|
|
70
|
-
const state = part.state || {};
|
|
77
|
+
const state = part?.state && typeof part.state === 'object' ? part.state : {};
|
|
71
78
|
if (this.onToolResult) this.onToolResult({
|
|
72
|
-
tool_id: part
|
|
73
|
-
output: state.output ?? part
|
|
79
|
+
tool_id: part?.callID || part?.id || event.tool_id || '',
|
|
80
|
+
output: state.output ?? part?.output ?? event.output ?? event.result ?? '',
|
|
74
81
|
status: state.status || event.status || (event.isError ? 'error' : 'ok'),
|
|
75
82
|
});
|
|
76
83
|
}
|