@terminai/a2a-server 0.21.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.
Files changed (154) hide show
  1. package/README.md +5 -0
  2. package/dist/.last_build +0 -0
  3. package/dist/a2a-server.mjs +415698 -0
  4. package/dist/index.d.ts +7 -0
  5. package/dist/index.js +8 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/agent/executor.d.ts +41 -0
  8. package/dist/src/agent/executor.js +408 -0
  9. package/dist/src/agent/executor.js.map +1 -0
  10. package/dist/src/agent/task.d.ts +67 -0
  11. package/dist/src/agent/task.js +799 -0
  12. package/dist/src/agent/task.js.map +1 -0
  13. package/dist/src/agent/task.test.d.ts +7 -0
  14. package/dist/src/agent/task.test.js +435 -0
  15. package/dist/src/agent/task.test.js.map +1 -0
  16. package/dist/src/agent/task.token.test.d.ts +7 -0
  17. package/dist/src/agent/task.token.test.js +53 -0
  18. package/dist/src/agent/task.token.test.js.map +1 -0
  19. package/dist/src/auth/llmAuthManager.d.ts +39 -0
  20. package/dist/src/auth/llmAuthManager.js +209 -0
  21. package/dist/src/auth/llmAuthManager.js.map +1 -0
  22. package/dist/src/auth/llmAuthManager.test.d.ts +7 -0
  23. package/dist/src/auth/llmAuthManager.test.js +92 -0
  24. package/dist/src/auth/llmAuthManager.test.js.map +1 -0
  25. package/dist/src/commands/command-registry.d.ts +16 -0
  26. package/dist/src/commands/command-registry.js +35 -0
  27. package/dist/src/commands/command-registry.js.map +1 -0
  28. package/dist/src/commands/command-registry.test.d.ts +7 -0
  29. package/dist/src/commands/command-registry.test.js +100 -0
  30. package/dist/src/commands/command-registry.test.js.map +1 -0
  31. package/dist/src/commands/extensions.d.ts +19 -0
  32. package/dist/src/commands/extensions.js +26 -0
  33. package/dist/src/commands/extensions.js.map +1 -0
  34. package/dist/src/commands/extensions.test.d.ts +7 -0
  35. package/dist/src/commands/extensions.test.js +70 -0
  36. package/dist/src/commands/extensions.test.js.map +1 -0
  37. package/dist/src/commands/init.d.ts +16 -0
  38. package/dist/src/commands/init.js +111 -0
  39. package/dist/src/commands/init.js.map +1 -0
  40. package/dist/src/commands/init.test.d.ts +7 -0
  41. package/dist/src/commands/init.test.js +146 -0
  42. package/dist/src/commands/init.test.js.map +1 -0
  43. package/dist/src/commands/restore.d.ts +21 -0
  44. package/dist/src/commands/restore.js +126 -0
  45. package/dist/src/commands/restore.js.map +1 -0
  46. package/dist/src/commands/restore.test.d.ts +7 -0
  47. package/dist/src/commands/restore.test.js +111 -0
  48. package/dist/src/commands/restore.test.js.map +1 -0
  49. package/dist/src/commands/types.d.ts +33 -0
  50. package/dist/src/commands/types.js +8 -0
  51. package/dist/src/commands/types.js.map +1 -0
  52. package/dist/src/config/config.d.ts +24 -0
  53. package/dist/src/config/config.js +140 -0
  54. package/dist/src/config/config.js.map +1 -0
  55. package/dist/src/config/extension.d.ts +12 -0
  56. package/dist/src/config/extension.js +105 -0
  57. package/dist/src/config/extension.js.map +1 -0
  58. package/dist/src/config/settings.d.ts +15 -0
  59. package/dist/src/config/settings.js +20 -0
  60. package/dist/src/config/settings.js.map +1 -0
  61. package/dist/src/config/settings.test.d.ts +7 -0
  62. package/dist/src/config/settings.test.js +170 -0
  63. package/dist/src/config/settings.test.js.map +1 -0
  64. package/dist/src/http/app.d.ts +17 -0
  65. package/dist/src/http/app.js +399 -0
  66. package/dist/src/http/app.js.map +1 -0
  67. package/dist/src/http/app.test.d.ts +7 -0
  68. package/dist/src/http/app.test.js +1048 -0
  69. package/dist/src/http/app.test.js.map +1 -0
  70. package/dist/src/http/auth.d.ts +21 -0
  71. package/dist/src/http/auth.js +55 -0
  72. package/dist/src/http/auth.js.map +1 -0
  73. package/dist/src/http/auth.test.d.ts +7 -0
  74. package/dist/src/http/auth.test.js +53 -0
  75. package/dist/src/http/auth.test.js.map +1 -0
  76. package/dist/src/http/authRoutes.test.d.ts +7 -0
  77. package/dist/src/http/authRoutes.test.js +169 -0
  78. package/dist/src/http/authRoutes.test.js.map +1 -0
  79. package/dist/src/http/cors.d.ts +8 -0
  80. package/dist/src/http/cors.js +96 -0
  81. package/dist/src/http/cors.js.map +1 -0
  82. package/dist/src/http/cors.test.d.ts +7 -0
  83. package/dist/src/http/cors.test.js +62 -0
  84. package/dist/src/http/cors.test.js.map +1 -0
  85. package/dist/src/http/deferredAuth.test.d.ts +7 -0
  86. package/dist/src/http/deferredAuth.test.js +45 -0
  87. package/dist/src/http/deferredAuth.test.js.map +1 -0
  88. package/dist/src/http/endpoints.test.d.ts +7 -0
  89. package/dist/src/http/endpoints.test.js +149 -0
  90. package/dist/src/http/endpoints.test.js.map +1 -0
  91. package/dist/src/http/llmAuthMiddleware.d.ts +9 -0
  92. package/dist/src/http/llmAuthMiddleware.js +37 -0
  93. package/dist/src/http/llmAuthMiddleware.js.map +1 -0
  94. package/dist/src/http/relay.d.ts +28 -0
  95. package/dist/src/http/relay.js +342 -0
  96. package/dist/src/http/relay.js.map +1 -0
  97. package/dist/src/http/relay.test.d.ts +7 -0
  98. package/dist/src/http/relay.test.js +149 -0
  99. package/dist/src/http/relay.test.js.map +1 -0
  100. package/dist/src/http/replay.d.ts +19 -0
  101. package/dist/src/http/replay.js +90 -0
  102. package/dist/src/http/replay.js.map +1 -0
  103. package/dist/src/http/replay.test.d.ts +7 -0
  104. package/dist/src/http/replay.test.js +78 -0
  105. package/dist/src/http/replay.test.js.map +1 -0
  106. package/dist/src/http/requestStorage.d.ts +11 -0
  107. package/dist/src/http/requestStorage.js +9 -0
  108. package/dist/src/http/requestStorage.js.map +1 -0
  109. package/dist/src/http/routes/auth.d.ts +9 -0
  110. package/dist/src/http/routes/auth.js +125 -0
  111. package/dist/src/http/routes/auth.js.map +1 -0
  112. package/dist/src/http/server.d.ts +8 -0
  113. package/dist/src/http/server.js +28 -0
  114. package/dist/src/http/server.js.map +1 -0
  115. package/dist/src/index.d.ts +10 -0
  116. package/dist/src/index.js +11 -0
  117. package/dist/src/index.js.map +1 -0
  118. package/dist/src/persistence/gcs.d.ts +25 -0
  119. package/dist/src/persistence/gcs.js +248 -0
  120. package/dist/src/persistence/gcs.js.map +1 -0
  121. package/dist/src/persistence/gcs.test.d.ts +7 -0
  122. package/dist/src/persistence/gcs.test.js +335 -0
  123. package/dist/src/persistence/gcs.test.js.map +1 -0
  124. package/dist/src/persistence/remoteAuthStore.d.ts +21 -0
  125. package/dist/src/persistence/remoteAuthStore.js +74 -0
  126. package/dist/src/persistence/remoteAuthStore.js.map +1 -0
  127. package/dist/src/types.d.ts +100 -0
  128. package/dist/src/types.js +49 -0
  129. package/dist/src/types.js.map +1 -0
  130. package/dist/src/utils/envAliases.d.ts +7 -0
  131. package/dist/src/utils/envAliases.js +9 -0
  132. package/dist/src/utils/envAliases.js.map +1 -0
  133. package/dist/src/utils/executor_utils.d.ts +8 -0
  134. package/dist/src/utils/executor_utils.js +42 -0
  135. package/dist/src/utils/executor_utils.js.map +1 -0
  136. package/dist/src/utils/logger.d.ts +9 -0
  137. package/dist/src/utils/logger.js +26 -0
  138. package/dist/src/utils/logger.js.map +1 -0
  139. package/dist/src/utils/redactSecrets.d.ts +16 -0
  140. package/dist/src/utils/redactSecrets.js +72 -0
  141. package/dist/src/utils/redactSecrets.js.map +1 -0
  142. package/dist/src/utils/redactSecrets.test.d.ts +7 -0
  143. package/dist/src/utils/redactSecrets.test.js +62 -0
  144. package/dist/src/utils/redactSecrets.test.js.map +1 -0
  145. package/dist/src/utils/testing_utils.d.ts +48 -0
  146. package/dist/src/utils/testing_utils.js +173 -0
  147. package/dist/src/utils/testing_utils.js.map +1 -0
  148. package/dist/tsconfig.tsbuildinfo +1 -0
  149. package/dist/web-client/app.js +526 -0
  150. package/dist/web-client/index.html +43 -0
  151. package/dist/web-client/package.json +10 -0
  152. package/dist/web-client/relay-client.js +330 -0
  153. package/dist/web-client/style.css +189 -0
  154. package/package.json +53 -0
@@ -0,0 +1,526 @@
1
+ import { RelayClient } from './relay-client.js';
2
+
3
+ const chat = document.getElementById('chat');
4
+ const input = document.getElementById('input');
5
+ const sendButton = document.getElementById('send');
6
+ const statusDot = document.getElementById('status-dot');
7
+ const statusText = document.getElementById('status-text');
8
+
9
+ let activeTaskId = null;
10
+ let currentAssistantEl = null;
11
+
12
+ // Relay State
13
+ let relayClient = null;
14
+
15
+ // Check for Relay Params
16
+ const fragment = new URLSearchParams(window.location.hash.substring(1));
17
+ const relayUrl = fragment.get('relay');
18
+ const sessionId = fragment.get('session');
19
+ const keyBase64 = fragment.get('key');
20
+
21
+ // Strip fragment to prevent key leakage
22
+ if (relayUrl && sessionId && keyBase64) {
23
+ history.replaceState(
24
+ null,
25
+ '',
26
+ window.location.pathname + window.location.search,
27
+ );
28
+ }
29
+
30
+ function setStatus(text, color = 'var(--muted)') {
31
+ statusDot.style.background = color;
32
+ statusText.textContent = text;
33
+ }
34
+
35
+ if (relayUrl && sessionId && keyBase64) {
36
+ console.log('Using Cloud Relay Mode');
37
+ relayClient = new RelayClient(
38
+ relayUrl,
39
+ sessionId,
40
+ keyBase64,
41
+ (msg) => handleA2aEvent(msg), // onMessage
42
+ (text, color) => setStatus(text, color), // onStatus
43
+ () => renderPairingPrompt(), // onPairingRequired
44
+ );
45
+ relayClient.connect();
46
+ // Wait for handshake before allowing UI interactions
47
+ setTimeout(() => {
48
+ if (relayClient && relayClient.state !== 'READY') {
49
+ setStatus('Waiting for handshake...', 'var(--muted)');
50
+ }
51
+ }, 1000);
52
+ }
53
+
54
+ function appendMessage(role, text) {
55
+ const el = document.createElement('div');
56
+ el.className = `message ${role}`;
57
+ el.textContent = text;
58
+ chat.appendChild(el);
59
+ chat.scrollTop = chat.scrollHeight;
60
+ return el;
61
+ }
62
+
63
+ function getToken() {
64
+ // If using Relay, we don't need a token
65
+ if (relayClient) return 'relay-mode';
66
+
67
+ const url = new URL(window.location.href);
68
+ const token = url.searchParams.get('token');
69
+ if (token) {
70
+ localStorage.setItem('termai_token', token);
71
+ url.searchParams.delete('token');
72
+ window.history.replaceState({}, '', url.toString());
73
+ return token;
74
+ }
75
+ return localStorage.getItem('termai_token') || '';
76
+ }
77
+
78
+ async function sha256Hex(text) {
79
+ const enc = new TextEncoder();
80
+ const digest = await crypto.subtle.digest('SHA-256', enc.encode(text));
81
+ return [...new Uint8Array(digest)]
82
+ .map((b) => b.toString(16).padStart(2, '0'))
83
+ .join('');
84
+ }
85
+
86
+ async function hmacSha256Hex(key, payload) {
87
+ const enc = new TextEncoder();
88
+ const cryptoKey = await crypto.subtle.importKey(
89
+ 'raw',
90
+ enc.encode(key),
91
+ { name: 'HMAC', hash: 'SHA-256' },
92
+ false,
93
+ ['sign'],
94
+ );
95
+ const signature = await crypto.subtle.sign(
96
+ 'HMAC',
97
+ cryptoKey,
98
+ enc.encode(payload),
99
+ );
100
+ return [...new Uint8Array(signature)]
101
+ .map((b) => b.toString(16).padStart(2, '0'))
102
+ .join('');
103
+ }
104
+
105
+ async function signedHeaders({ token, method, pathWithQuery, bodyString }) {
106
+ const nonce = crypto.randomUUID();
107
+ const bodyHash = await sha256Hex(bodyString);
108
+ const payload = [method.toUpperCase(), pathWithQuery, bodyHash, nonce].join(
109
+ '\n',
110
+ );
111
+ const signature = await hmacSha256Hex(token, payload);
112
+ return {
113
+ Authorization: `Bearer ${token}`,
114
+ 'X-Gemini-Nonce': nonce,
115
+ 'X-Gemini-Signature': signature,
116
+ };
117
+ }
118
+
119
+ async function readSse(response, onEvent) {
120
+ const reader = response.body.getReader();
121
+ const decoder = new TextDecoder();
122
+ let buffer = '';
123
+
124
+ while (true) {
125
+ const { value, done } = await reader.read();
126
+ if (done) break;
127
+ buffer += decoder.decode(value, { stream: true });
128
+ const parts = buffer.split('\n\n');
129
+ buffer = parts.pop() || '';
130
+ for (const chunk of parts) {
131
+ const lines = chunk.split('\n').filter((l) => l.startsWith('data:'));
132
+ for (const line of lines) {
133
+ const data = line.slice('data:'.length).trimStart();
134
+ if (!data) continue;
135
+ try {
136
+ onEvent(JSON.parse(data));
137
+ } catch {
138
+ // ignore malformed event
139
+ }
140
+ }
141
+ }
142
+ }
143
+ }
144
+
145
+ function ensureAssistantMessage() {
146
+ if (!currentAssistantEl) {
147
+ currentAssistantEl = appendMessage('ai', '');
148
+ }
149
+ return currentAssistantEl;
150
+ }
151
+
152
+ function clearAssistantMessage() {
153
+ currentAssistantEl = null;
154
+ }
155
+
156
+ function renderPairingPrompt() {
157
+ // Remove any existing pairing prompt
158
+ const existing = document.getElementById('pairing-prompt');
159
+ if (existing) existing.remove();
160
+
161
+ const wrapper = document.createElement('div');
162
+ wrapper.id = 'pairing-prompt';
163
+ wrapper.className = 'message ai';
164
+ wrapper.style.maxWidth = '100%';
165
+
166
+ const title = document.createElement('div');
167
+ title.textContent = 'Pairing Required';
168
+ title.style.fontWeight = '700';
169
+ title.style.marginBottom = '6px';
170
+
171
+ const body = document.createElement('div');
172
+ body.textContent =
173
+ 'Enter the 6-digit pairing code shown on the host terminal:';
174
+ body.style.marginBottom = '10px';
175
+
176
+ const codeInput = document.createElement('input');
177
+ codeInput.type = 'text';
178
+ codeInput.placeholder = 'Enter code (6 digits)';
179
+ codeInput.inputMode = 'numeric';
180
+ codeInput.pattern = '\\d*';
181
+ codeInput.maxLength = 6;
182
+ codeInput.style.width = '100%';
183
+ codeInput.style.marginBottom = '10px';
184
+ codeInput.style.padding = '10px 12px';
185
+ codeInput.style.borderRadius = '12px';
186
+ codeInput.style.border = '1px solid var(--border)';
187
+ codeInput.style.background = 'rgba(255,255,255,0.03)';
188
+ codeInput.style.color = 'var(--text)';
189
+ codeInput.style.fontSize = '18px';
190
+ codeInput.style.textAlign = 'center';
191
+ codeInput.style.letterSpacing = '0.3em';
192
+
193
+ const submitBtn = document.createElement('button');
194
+ submitBtn.textContent = 'Pair';
195
+ submitBtn.style.width = '100%';
196
+ submitBtn.disabled = true;
197
+
198
+ const validateCode = () => {
199
+ const val = (codeInput.value || '').replace(/\D/g, '');
200
+ codeInput.value = val.slice(0, 6);
201
+ return codeInput.value.length === 6;
202
+ };
203
+
204
+ codeInput.addEventListener('input', () => {
205
+ submitBtn.disabled = !validateCode();
206
+ });
207
+
208
+ submitBtn.addEventListener('click', async () => {
209
+ const code = codeInput.value.trim();
210
+ if (code.length !== 6) return;
211
+ submitBtn.disabled = true;
212
+ submitBtn.textContent = 'Pairing...';
213
+ try {
214
+ await relayClient.sendPairingCode(code);
215
+ // Success will be handled by PAIR_ACK message
216
+ wrapper.remove();
217
+ } catch (e) {
218
+ submitBtn.textContent = 'Pair';
219
+ submitBtn.disabled = false;
220
+ console.error('Pairing error:', e);
221
+ }
222
+ });
223
+
224
+ // Allow Enter key to submit
225
+ codeInput.addEventListener('keydown', (e) => {
226
+ if (e.key === 'Enter' && !submitBtn.disabled) {
227
+ submitBtn.click();
228
+ }
229
+ });
230
+
231
+ wrapper.appendChild(title);
232
+ wrapper.appendChild(body);
233
+ wrapper.appendChild(codeInput);
234
+ wrapper.appendChild(submitBtn);
235
+ chat.appendChild(wrapper);
236
+ chat.scrollTop = chat.scrollHeight;
237
+ codeInput.focus();
238
+ }
239
+
240
+ function renderConfirmation({ callId, prompt, requiresPin, pinLength }) {
241
+ const wrapper = document.createElement('div');
242
+ wrapper.className = 'message ai';
243
+ wrapper.style.maxWidth = '100%';
244
+
245
+ const title = document.createElement('div');
246
+ title.textContent = 'Confirmation required';
247
+ title.style.fontWeight = '700';
248
+ title.style.marginBottom = '6px';
249
+
250
+ const body = document.createElement('div');
251
+ body.textContent = prompt;
252
+ body.style.whiteSpace = 'pre-wrap';
253
+ body.style.marginBottom = '10px';
254
+
255
+ const pinInput = document.createElement('input');
256
+ pinInput.type = 'password';
257
+ pinInput.placeholder = requiresPin ? `PIN (${pinLength} digits)` : '';
258
+ pinInput.inputMode = 'numeric';
259
+ pinInput.pattern = '\\d*';
260
+ pinInput.style.width = '100%';
261
+ pinInput.style.marginBottom = '10px';
262
+ pinInput.style.padding = '10px 12px';
263
+ pinInput.style.borderRadius = '12px';
264
+ pinInput.style.border = '1px solid var(--border)';
265
+ pinInput.style.background = 'rgba(255,255,255,0.03)';
266
+ pinInput.style.color = 'var(--text)';
267
+ pinInput.style.display = requiresPin ? 'block' : 'none';
268
+
269
+ const actions = document.createElement('div');
270
+ actions.style.display = 'flex';
271
+ actions.style.gap = '10px';
272
+
273
+ const yes = document.createElement('button');
274
+ yes.textContent = 'Yes, proceed';
275
+ yes.style.flex = '1';
276
+
277
+ const no = document.createElement('button');
278
+ no.textContent = 'Cancel';
279
+ no.style.flex = '1';
280
+ no.style.background = 'rgba(255,255,255,0.08)';
281
+ no.style.color = 'var(--text)';
282
+ no.style.boxShadow = 'none';
283
+ no.style.border = '1px solid var(--border)';
284
+
285
+ const validatePin = () => {
286
+ if (!requiresPin) return true;
287
+ const val = (pinInput.value || '').replace(/\D/g, '');
288
+ pinInput.value = val.slice(0, pinLength);
289
+ return pinInput.value.length === pinLength;
290
+ };
291
+
292
+ pinInput.addEventListener('input', () => {
293
+ validatePin();
294
+ yes.disabled = requiresPin && !validatePin();
295
+ });
296
+
297
+ yes.disabled = requiresPin;
298
+
299
+ yes.addEventListener('click', async () => {
300
+ const pin = requiresPin
301
+ ? (pinInput.value || '').replace(/\D/g, '')
302
+ : undefined;
303
+ if (requiresPin && (!pin || pin.length !== pinLength)) return;
304
+ await sendToolConfirmation(callId, true, pin);
305
+ wrapper.remove();
306
+ });
307
+
308
+ no.addEventListener('click', async () => {
309
+ const pin = requiresPin
310
+ ? (pinInput.value || '').replace(/\D/g, '')
311
+ : undefined;
312
+ await sendToolConfirmation(callId, false, pin);
313
+ wrapper.remove();
314
+ });
315
+
316
+ actions.appendChild(yes);
317
+ actions.appendChild(no);
318
+
319
+ wrapper.appendChild(title);
320
+ wrapper.appendChild(body);
321
+ wrapper.appendChild(pinInput);
322
+ wrapper.appendChild(actions);
323
+ chat.appendChild(wrapper);
324
+ chat.scrollTop = chat.scrollHeight;
325
+ }
326
+
327
+ async function postStream(body) {
328
+ // If Relay Mode
329
+ if (relayClient) {
330
+ await relayClient.send(body);
331
+ return { ok: true, relayMode: true };
332
+ }
333
+
334
+ const token = getToken();
335
+ if (!token) {
336
+ setStatus('Missing token (open /ui?token=...)', 'var(--danger)');
337
+ appendMessage(
338
+ 'error',
339
+ 'Missing token. Start web-remote and open the URL with ?token=...',
340
+ );
341
+ throw new Error('Missing token');
342
+ }
343
+
344
+ const bodyString = JSON.stringify(body);
345
+ const headers = await signedHeaders({
346
+ token,
347
+ method: 'POST',
348
+ pathWithQuery: '/',
349
+ bodyString,
350
+ });
351
+
352
+ return fetch('/', {
353
+ method: 'POST',
354
+ headers: {
355
+ ...headers,
356
+ 'Content-Type': 'application/json',
357
+ Accept: 'text/event-stream',
358
+ },
359
+ body: bodyString,
360
+ });
361
+ }
362
+
363
+ async function sendToolConfirmation(callId, approved, pin) {
364
+ if (!activeTaskId) {
365
+ appendMessage('error', 'No active task. Send a message first.');
366
+ return;
367
+ }
368
+ setStatus('Confirming…', 'var(--accent)');
369
+ clearAssistantMessage();
370
+
371
+ const body = {
372
+ jsonrpc: '2.0',
373
+ id: '1',
374
+ method: 'message/stream',
375
+ params: {
376
+ message: {
377
+ kind: 'message',
378
+ role: 'user',
379
+ parts: [
380
+ {
381
+ kind: 'data',
382
+ data: {
383
+ callId,
384
+ outcome: approved ? 'proceed_once' : 'cancel',
385
+ ...(pin ? { pin } : {}),
386
+ },
387
+ },
388
+ ],
389
+ messageId: crypto.randomUUID(),
390
+ },
391
+ metadata: {
392
+ coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
393
+ },
394
+ taskId: activeTaskId,
395
+ },
396
+ };
397
+
398
+ const response = await postStream(body);
399
+ if (response.relayMode) return;
400
+
401
+ if (!response.ok || !response.body) {
402
+ appendMessage(
403
+ 'error',
404
+ `Confirmation failed: ${response.status} ${response.statusText}`,
405
+ );
406
+ setStatus('Offline', 'var(--danger)');
407
+ return;
408
+ }
409
+
410
+ await readSse(response, handleA2aEvent);
411
+ setStatus('Connected', 'var(--accent)');
412
+ }
413
+
414
+ function handleA2aEvent(evt) {
415
+ if (!evt || !evt.result) return;
416
+ const result = evt.result;
417
+
418
+ if (result.kind === 'task' && typeof result.id === 'string') {
419
+ activeTaskId = result.id;
420
+ return;
421
+ }
422
+
423
+ if (result.kind !== 'status-update') return;
424
+
425
+ const coder = result.metadata?.coderAgent;
426
+ const kind = coder?.kind;
427
+
428
+ if (kind === 'text-content') {
429
+ const parts = result.status?.message?.parts || [];
430
+ for (const part of parts) {
431
+ if (part?.kind === 'text' && typeof part.text === 'string') {
432
+ const el = ensureAssistantMessage();
433
+ el.textContent += part.text;
434
+ chat.scrollTop = chat.scrollHeight;
435
+ }
436
+ }
437
+ }
438
+
439
+ if (kind === 'tool-call-confirmation') {
440
+ const part = (result.status?.message?.parts || []).find(
441
+ (p) => p?.kind === 'data' && p.data,
442
+ );
443
+ const tool = part?.data || {};
444
+ const callId = tool?.request?.callId;
445
+ const prompt =
446
+ tool?.confirmationDetails?.prompt ||
447
+ tool?.confirmationDetails?.command ||
448
+ 'Confirm tool execution';
449
+ const requiresPin = tool?.confirmationDetails?.requiresPin === true;
450
+ const pinLength =
451
+ typeof tool?.confirmationDetails?.pinLength === 'number'
452
+ ? tool.confirmationDetails.pinLength
453
+ : 6;
454
+ if (callId) {
455
+ renderConfirmation({ callId, prompt, requiresPin, pinLength });
456
+ }
457
+ }
458
+
459
+ if (result.final === true) {
460
+ setStatus('Connected', 'var(--accent)');
461
+ clearAssistantMessage();
462
+ // Re-enable send button if disabled?
463
+ }
464
+ }
465
+
466
+ async function sendMessage() {
467
+ const value = input.value.trim();
468
+ if (!value) return;
469
+ appendMessage('user', value);
470
+ input.value = '';
471
+ clearAssistantMessage();
472
+ setStatus('Sending…', 'var(--accent)');
473
+
474
+ const body = {
475
+ jsonrpc: '2.0',
476
+ id: '1',
477
+ method: 'message/stream',
478
+ params: {
479
+ message: {
480
+ kind: 'message',
481
+ role: 'user',
482
+ parts: [{ kind: 'text', text: value }],
483
+ messageId: crypto.randomUUID(),
484
+ },
485
+ metadata: {
486
+ coderAgent: { kind: 'agent-settings', workspacePath: '/tmp' },
487
+ },
488
+ ...(activeTaskId ? { taskId: activeTaskId } : {}),
489
+ },
490
+ };
491
+
492
+ try {
493
+ const response = await postStream(body);
494
+ if (response.relayMode) return;
495
+
496
+ if (!response.ok || !response.body) {
497
+ appendMessage(
498
+ 'error',
499
+ `Request failed: ${response.status} ${response.statusText}`,
500
+ );
501
+ setStatus('Offline', 'var(--danger)');
502
+ return;
503
+ }
504
+ await readSse(response, handleA2aEvent);
505
+ setStatus('Connected', 'var(--accent)');
506
+ } catch (error) {
507
+ appendMessage('error', error?.message || 'Failed to reach server.');
508
+ setStatus('Offline', 'var(--danger)');
509
+ }
510
+ }
511
+
512
+ sendButton.addEventListener('click', () => {
513
+ void sendMessage();
514
+ });
515
+
516
+ input.addEventListener('keydown', (event) => {
517
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
518
+ event.preventDefault();
519
+ void sendMessage();
520
+ }
521
+ });
522
+
523
+ setStatus(
524
+ getToken() ? 'Ready' : 'Missing token',
525
+ getToken() ? 'var(--accent)' : 'var(--danger)',
526
+ );
@@ -0,0 +1,43 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>TerminaI Remote</title>
7
+ <link rel="stylesheet" href="style.css" />
8
+ </head>
9
+ <body>
10
+ <div id="app">
11
+ <header>
12
+ <div>
13
+ <p class="eyebrow">TerminaI Remote</p>
14
+ <h1>Stay in flow from your phone</h1>
15
+ <p class="lede">
16
+ Send quick prompts, watch responses stream, and keep the terminal in
17
+ sync.
18
+ </p>
19
+ </div>
20
+ <div class="status">
21
+ <span class="dot" id="status-dot"></span>
22
+ <span id="status-text">Connecting…</span>
23
+ </div>
24
+ </header>
25
+
26
+ <main id="chat"></main>
27
+
28
+ <footer>
29
+ <label for="input">Ask TerminaI</label>
30
+ <textarea
31
+ id="input"
32
+ placeholder="Run tests, summarize logs, or ask for a quick fix…"
33
+ rows="3"
34
+ ></textarea>
35
+ <div class="actions">
36
+ <button id="send">Send</button>
37
+ <small>Press Ctrl/Cmd+Enter to send</small>
38
+ </div>
39
+ </footer>
40
+ </div>
41
+ <script src="app.js" type="module"></script>
42
+ </body>
43
+ </html>
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "@terminai/web-client",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "description": "Minimal static client for TerminaI web-remote",
6
+ "license": "Apache-2.0",
7
+ "scripts": {
8
+ "build": "echo 'No build needed for static web client'"
9
+ }
10
+ }