plazbot-cli 0.3.3 → 0.3.5
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/studio/components/App.js +38 -31
- package/dist/studio/components/ChatLog.js +49 -21
- package/dist/studio/components/ConnectCard.js +85 -0
- package/dist/studio/components/Footer.js +1 -1
- package/dist/studio/components/Input.js +143 -11
- package/dist/studio/connect/api.js +66 -0
- package/dist/studio/connect/polling.js +76 -0
- package/dist/studio/connect/types.js +72 -0
- package/dist/studio/runRepl.js +6 -37
- package/dist/studio/state/store.js +13 -12
- package/package.json +1 -1
- package/src/studio/components/App.tsx +41 -33
- package/src/studio/components/ChatLog.tsx +58 -32
- package/src/studio/components/{WhatsappConnectCard.tsx → ConnectCard.tsx} +54 -22
- package/src/studio/components/Footer.tsx +1 -1
- package/src/studio/components/Input.tsx +159 -15
- package/src/studio/{whatsapp → connect}/api.ts +11 -9
- package/src/studio/{whatsapp → connect}/polling.ts +26 -25
- package/src/studio/connect/types.ts +139 -0
- package/src/studio/runRepl.tsx +6 -37
- package/src/studio/state/store.ts +26 -20
- package/dist/studio/components/WhatsappConnectCard.js +0 -57
- package/src/studio/whatsapp/types.ts +0 -80
|
@@ -1,32 +1,176 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
import { Box, Text } from 'ink';
|
|
3
|
-
import
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { Box, Text, useInput } from 'ink';
|
|
3
|
+
import { useStudioStore } from '../state/store.js';
|
|
4
4
|
|
|
5
5
|
interface InputProps {
|
|
6
6
|
disabled: boolean;
|
|
7
7
|
onSubmit: (value: string) => void;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Caja de texto del REPL. Implementación custom (sin ink-text-input) para poder
|
|
12
|
+
* soportar:
|
|
13
|
+
*
|
|
14
|
+
* - Historial estilo bash: ↑/↓ navega por mensajes previos del usuario.
|
|
15
|
+
* - Ghost text estilo zsh autosuggestions: muestra en gris la coincidencia
|
|
16
|
+
* más reciente del historial cuyo prefijo es lo que el usuario ya tipeó.
|
|
17
|
+
* Se acepta con Tab o → (solo cuando el cursor está al final del texto).
|
|
18
|
+
*
|
|
19
|
+
* Esc y Ctrl+C no se manejan aquí — burbujean al useInput global de App para
|
|
20
|
+
* que cancele stream / haga exit, sin cambiar el comportamiento existente.
|
|
21
|
+
*/
|
|
10
22
|
export function Input({ disabled, onSubmit }: InputProps): React.ReactElement {
|
|
11
23
|
const [value, setValue] = useState('');
|
|
24
|
+
const [cursor, setCursor] = useState(0);
|
|
25
|
+
// -1 = no estamos navegando historial (ghost activo, texto editable libre).
|
|
26
|
+
// 0 = mensaje más reciente; aumenta hacia mensajes más viejos.
|
|
27
|
+
const [historyIdx, setHistoryIdx] = useState(-1);
|
|
28
|
+
|
|
29
|
+
// Historial derivado del store: solo mensajes del usuario, deduplicados
|
|
30
|
+
// consecutivamente, más recientes primero.
|
|
31
|
+
const history = useStudioStore((s) => {
|
|
32
|
+
const out: string[] = [];
|
|
33
|
+
for (let i = s.messages.length - 1; i >= 0; i--) {
|
|
34
|
+
const m = s.messages[i];
|
|
35
|
+
if (m.role !== 'user') continue;
|
|
36
|
+
if (out[out.length - 1] === m.content) continue;
|
|
37
|
+
out.push(m.content);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Ghost: primer match del historial cuyo prefijo es `value`. Solo se muestra
|
|
43
|
+
// mientras NO estemos navegando con flechas (para no confundir al usuario).
|
|
44
|
+
const ghost = useMemo(() => {
|
|
45
|
+
if (!value || historyIdx >= 0) return '';
|
|
46
|
+
for (const h of history) {
|
|
47
|
+
if (h.length > value.length && h.startsWith(value)) {
|
|
48
|
+
return h.slice(value.length);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return '';
|
|
52
|
+
}, [value, history, historyIdx]);
|
|
53
|
+
|
|
54
|
+
const resetAfterSubmit = () => {
|
|
55
|
+
setValue('');
|
|
56
|
+
setCursor(0);
|
|
57
|
+
setHistoryIdx(-1);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
useInput(
|
|
61
|
+
(input, key) => {
|
|
62
|
+
if (disabled) return;
|
|
63
|
+
|
|
64
|
+
// Enter → submit
|
|
65
|
+
if (key.return) {
|
|
66
|
+
if (!value.trim()) return;
|
|
67
|
+
const toSend = value;
|
|
68
|
+
resetAfterSubmit();
|
|
69
|
+
onSubmit(toSend);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ↑ → mensaje más viejo
|
|
74
|
+
if (key.upArrow) {
|
|
75
|
+
if (history.length === 0) return;
|
|
76
|
+
const next = Math.min(historyIdx + 1, history.length - 1);
|
|
77
|
+
const text = history[next];
|
|
78
|
+
setHistoryIdx(next);
|
|
79
|
+
setValue(text);
|
|
80
|
+
setCursor(text.length);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ↓ → mensaje más reciente / volver al input vacío
|
|
85
|
+
if (key.downArrow) {
|
|
86
|
+
if (historyIdx <= 0) {
|
|
87
|
+
setHistoryIdx(-1);
|
|
88
|
+
setValue('');
|
|
89
|
+
setCursor(0);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const next = historyIdx - 1;
|
|
93
|
+
const text = history[next];
|
|
94
|
+
setHistoryIdx(next);
|
|
95
|
+
setValue(text);
|
|
96
|
+
setCursor(text.length);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Tab → aceptar ghost completo
|
|
101
|
+
if (key.tab) {
|
|
102
|
+
if (ghost) {
|
|
103
|
+
const full = value + ghost;
|
|
104
|
+
setValue(full);
|
|
105
|
+
setCursor(full.length);
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// → → mover cursor; si está al final y hay ghost, acepta el ghost
|
|
111
|
+
if (key.rightArrow) {
|
|
112
|
+
if (cursor < value.length) {
|
|
113
|
+
setCursor(cursor + 1);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (ghost) {
|
|
117
|
+
const full = value + ghost;
|
|
118
|
+
setValue(full);
|
|
119
|
+
setCursor(full.length);
|
|
120
|
+
}
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ← → mover cursor
|
|
125
|
+
if (key.leftArrow) {
|
|
126
|
+
if (cursor > 0) setCursor(cursor - 1);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Backspace / Delete → borrar antes del cursor
|
|
131
|
+
if (key.backspace || key.delete) {
|
|
132
|
+
if (cursor === 0) return;
|
|
133
|
+
const next = value.slice(0, cursor - 1) + value.slice(cursor);
|
|
134
|
+
setValue(next);
|
|
135
|
+
setCursor(cursor - 1);
|
|
136
|
+
if (historyIdx >= 0) setHistoryIdx(-1);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Esc, Ctrl+*, Meta → no consumir, los maneja App.tsx
|
|
141
|
+
if (key.escape || key.ctrl || key.meta) return;
|
|
142
|
+
|
|
143
|
+
// Char inputs (incluye pegado multibyte). Filtramos secuencias raras
|
|
144
|
+
// que Ink a veces entrega como `input` vacío.
|
|
145
|
+
if (input && input.length > 0) {
|
|
146
|
+
// Si veníamos de navegar historial y el usuario escribe algo, salimos
|
|
147
|
+
// del modo navegación y empezamos a editar normalmente.
|
|
148
|
+
if (historyIdx >= 0) setHistoryIdx(-1);
|
|
149
|
+
const next = value.slice(0, cursor) + input + value.slice(cursor);
|
|
150
|
+
setValue(next);
|
|
151
|
+
setCursor(cursor + input.length);
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
{ isActive: !disabled },
|
|
155
|
+
);
|
|
12
156
|
|
|
13
157
|
return (
|
|
14
158
|
<Box borderStyle="round" borderColor={disabled ? 'gray' : 'green'} paddingX={1}>
|
|
15
159
|
<Text color={disabled ? 'gray' : 'green'} bold>
|
|
16
160
|
{disabled ? '·' : '›'}{' '}
|
|
17
161
|
</Text>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
162
|
+
{disabled ? (
|
|
163
|
+
<Text dimColor>Waiting for response… (press Esc to cancel)</Text>
|
|
164
|
+
) : value.length === 0 ? (
|
|
165
|
+
<Text dimColor>Type a message or /help</Text>
|
|
166
|
+
) : (
|
|
167
|
+
<Text>
|
|
168
|
+
<Text>{value.slice(0, cursor)}</Text>
|
|
169
|
+
<Text inverse>{value.slice(cursor, cursor + 1) || ' '}</Text>
|
|
170
|
+
<Text>{value.slice(cursor + 1)}</Text>
|
|
171
|
+
{ghost ? <Text dimColor>{ghost}</Text> : null}
|
|
172
|
+
</Text>
|
|
173
|
+
)}
|
|
30
174
|
</Box>
|
|
31
175
|
);
|
|
32
176
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { getBaseUrl } from '../api/studioApi.js';
|
|
2
2
|
import type { StudioStreamOptions } from '../api/types.js';
|
|
3
|
-
import type
|
|
3
|
+
import { platformCodeForChannel, type ConnectChannel, type WorkspaceIntegrationItem } from './types.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* REST
|
|
7
|
-
*
|
|
6
|
+
* Helpers REST para el polling/activacion durante la conexion de un canal.
|
|
7
|
+
* Reusa el mismo base URL + Bearer del SSE client.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
interface WorkspaceFetchOpts {
|
|
@@ -31,8 +31,8 @@ function headers(opts: WorkspaceFetchOpts): Record<string, string> {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* GET /api/workspace/{workspaceId} —
|
|
35
|
-
* (
|
|
34
|
+
* GET /api/workspace/{workspaceId} — el backend a veces devuelve un array
|
|
35
|
+
* (el front usa `[0]`). Devuelve el primer elemento o null.
|
|
36
36
|
*/
|
|
37
37
|
export async function getWorkspaceById(
|
|
38
38
|
opts: WorkspaceFetchOpts,
|
|
@@ -56,10 +56,13 @@ export interface ActivateResult {
|
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* POST /api/workspace/{workspaceId}/integrations/{integrationId}/activate
|
|
59
|
-
* Body: { isActive: true, platformCode
|
|
59
|
+
* Body: { isActive: true, platformCode }
|
|
60
|
+
*
|
|
61
|
+
* `platformCode` se deriva del canal logico (whatsapp/instagram/facebook).
|
|
60
62
|
*/
|
|
61
63
|
export async function activateIntegration(
|
|
62
64
|
opts: WorkspaceFetchOpts,
|
|
65
|
+
channel: ConnectChannel,
|
|
63
66
|
integrationId: string,
|
|
64
67
|
signal?: AbortSignal,
|
|
65
68
|
): Promise<ActivateResult> {
|
|
@@ -67,7 +70,7 @@ export async function activateIntegration(
|
|
|
67
70
|
const res = await fetch(url, {
|
|
68
71
|
method: 'POST',
|
|
69
72
|
headers: headers(opts),
|
|
70
|
-
body: JSON.stringify({ isActive: true, platformCode:
|
|
73
|
+
body: JSON.stringify({ isActive: true, platformCode: platformCodeForChannel(channel) }),
|
|
71
74
|
signal,
|
|
72
75
|
});
|
|
73
76
|
const text = await res.text();
|
|
@@ -81,10 +84,9 @@ export async function activateIntegration(
|
|
|
81
84
|
return parsed.success === false ? parsed : { success: true, ...parsed };
|
|
82
85
|
}
|
|
83
86
|
|
|
84
|
-
/** Re-export for callers that need just the auth shape. */
|
|
85
87
|
export type WorkspaceAuthOpts = WorkspaceFetchOpts;
|
|
86
88
|
|
|
87
|
-
/** Adapter:
|
|
89
|
+
/** Adapter: deriva las opciones de fetch desde un StudioStreamOptions. */
|
|
88
90
|
export function workspaceOptsFromStream(stream: StudioStreamOptions): WorkspaceAuthOpts {
|
|
89
91
|
return {
|
|
90
92
|
apiKey: stream.apiKey,
|
|
@@ -1,34 +1,38 @@
|
|
|
1
1
|
import { activateIntegration, getWorkspaceById, type WorkspaceAuthOpts } from './api.js';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
CONNECT_POLL_INTERVAL_MS,
|
|
4
|
+
CONNECT_POLL_MAX_ATTEMPTS,
|
|
5
|
+
describeIntegration,
|
|
6
|
+
findNewIntegrationForChannel,
|
|
7
|
+
type ChannelLinkData,
|
|
8
|
+
type ConnectChannel,
|
|
7
9
|
} from './types.js';
|
|
8
10
|
|
|
9
11
|
export interface PollingCallbacks {
|
|
10
12
|
onAttempt(attempt: number): void;
|
|
11
|
-
onActivating(integrationId: string,
|
|
12
|
-
onConnected(
|
|
13
|
+
onActivating(integrationId: string, label: string): void;
|
|
14
|
+
onConnected(label: string): void;
|
|
13
15
|
onTimeout(): void;
|
|
14
16
|
onError(message: string): void;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface PollingHandle {
|
|
18
20
|
cancel(): void;
|
|
19
|
-
/** True
|
|
21
|
+
/** True una vez que el loop se detuvo (success, timeout, error o cancel). */
|
|
20
22
|
readonly stopped: boolean;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
26
|
+
* Inicia el polling de conexion de canal. Devuelve un handle para cancelarlo.
|
|
27
|
+
* El loop:
|
|
28
|
+
* 1. GET workspace cada CONNECT_POLL_INTERVAL_MS hasta CONNECT_POLL_MAX_ATTEMPTS.
|
|
29
|
+
* 2. Busca una integracion del `channel` cuyo id NO este en `existingIntegrationIds`.
|
|
30
|
+
* 3. Cuando la encuentra, POST .../activate y reporta el resultado.
|
|
28
31
|
*/
|
|
29
|
-
export function
|
|
32
|
+
export function startConnectPolling(
|
|
30
33
|
opts: WorkspaceAuthOpts,
|
|
31
|
-
|
|
34
|
+
channel: ConnectChannel,
|
|
35
|
+
linkData: ChannelLinkData,
|
|
32
36
|
cb: PollingCallbacks,
|
|
33
37
|
): PollingHandle {
|
|
34
38
|
const knownIds = new Set(linkData.existingIntegrationIds ?? []);
|
|
@@ -51,17 +55,14 @@ export function startWhatsappPolling(
|
|
|
51
55
|
cb.onAttempt(attempts);
|
|
52
56
|
try {
|
|
53
57
|
const ws = await getWorkspaceById(opts, controller.signal);
|
|
54
|
-
const newIntegration =
|
|
58
|
+
const newIntegration = findNewIntegrationForChannel(channel, ws?.integrations, knownIds);
|
|
55
59
|
if (newIntegration?.id) {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
newIntegration.cellphoneNumber ||
|
|
59
|
-
'';
|
|
60
|
-
cb.onActivating(newIntegration.id, number);
|
|
60
|
+
const label = describeIntegration(channel, newIntegration);
|
|
61
|
+
cb.onActivating(newIntegration.id, label);
|
|
61
62
|
if (timer) { clearInterval(timer); timer = null; }
|
|
62
|
-
const res = await activateIntegration(opts, newIntegration.id, controller.signal);
|
|
63
|
+
const res = await activateIntegration(opts, channel, newIntegration.id, controller.signal);
|
|
63
64
|
if (res.success !== false) {
|
|
64
|
-
cb.onConnected(
|
|
65
|
+
cb.onConnected(label);
|
|
65
66
|
} else {
|
|
66
67
|
cb.onError(res.message || 'Could not activate the integration.');
|
|
67
68
|
}
|
|
@@ -70,20 +71,20 @@ export function startWhatsappPolling(
|
|
|
70
71
|
}
|
|
71
72
|
} catch (err) {
|
|
72
73
|
if ((err as Error).name === 'AbortError') return;
|
|
73
|
-
//
|
|
74
|
+
// Network transient: swallow and keep polling.
|
|
74
75
|
} finally {
|
|
75
76
|
busy = false;
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
if (attempts >=
|
|
79
|
+
if (attempts >= CONNECT_POLL_MAX_ATTEMPTS) {
|
|
79
80
|
if (timer) { clearInterval(timer); timer = null; }
|
|
80
81
|
stopped = true;
|
|
81
82
|
cb.onTimeout();
|
|
82
83
|
}
|
|
83
84
|
};
|
|
84
85
|
|
|
85
|
-
timer = setInterval(() => { void tick(); },
|
|
86
|
-
//
|
|
86
|
+
timer = setInterval(() => { void tick(); }, CONNECT_POLL_INTERVAL_MS);
|
|
87
|
+
// Primer attempt inmediato para feedback visual.
|
|
87
88
|
void tick();
|
|
88
89
|
|
|
89
90
|
return {
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tipos y helpers del flujo "Conectar canal desde Agent Studio".
|
|
3
|
+
*
|
|
4
|
+
* El backend emite `tool_result` con `type` = `whatsapp_link` |
|
|
5
|
+
* `instagram_link` | `messenger_link` segun la tool invocada. Todos comparten
|
|
6
|
+
* el mismo shape de datos (link corto + expiracion + baseline de integraciones)
|
|
7
|
+
* y el mismo patron de polling (GET workspace cada 5s hasta detectar el nuevo
|
|
8
|
+
* id) y activacion (POST .../activate).
|
|
9
|
+
*
|
|
10
|
+
* Este modulo es la version generalizada de lo que antes vivia en
|
|
11
|
+
* `src/studio/whatsapp/` cuando solo existia WhatsApp.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export type ConnectChannel = 'whatsapp' | 'instagram' | 'messenger';
|
|
15
|
+
|
|
16
|
+
/** Mapea el `type` del tool_result al canal logico del cliente. */
|
|
17
|
+
export function channelFromToolResultType(type: string | undefined): ConnectChannel | null {
|
|
18
|
+
if (type === 'whatsapp_link') return 'whatsapp';
|
|
19
|
+
if (type === 'instagram_link') return 'instagram';
|
|
20
|
+
if (type === 'messenger_link') return 'messenger';
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Tipos de integracion (workspace.integrations[].type) a vigilar por canal. */
|
|
25
|
+
export function integrationTypesForChannel(channel: ConnectChannel): string[] {
|
|
26
|
+
switch (channel) {
|
|
27
|
+
case 'whatsapp': return ['whatsapp', 'whatsapp_business'];
|
|
28
|
+
case 'instagram': return ['instagram'];
|
|
29
|
+
case 'messenger': return ['facebook']; // Messenger se guarda como type=facebook
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* `platformCode` que espera POST .../activate. Para WhatsApp el endpoint usa
|
|
35
|
+
* este valor para una validacion de duplicados; para IG/Messenger no ejecuta
|
|
36
|
+
* ninguna logica especifica pero debe enviarse algo coherente.
|
|
37
|
+
*/
|
|
38
|
+
export function platformCodeForChannel(channel: ConnectChannel): string {
|
|
39
|
+
switch (channel) {
|
|
40
|
+
case 'whatsapp': return 'whatsapp';
|
|
41
|
+
case 'instagram': return 'instagram';
|
|
42
|
+
case 'messenger': return 'facebook';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ChannelLinkData {
|
|
47
|
+
shortUrl?: string | null;
|
|
48
|
+
/** whatsapp | whatsapp_business | instagram | facebook */
|
|
49
|
+
linkType?: string;
|
|
50
|
+
expiresAt?: string;
|
|
51
|
+
isNewLink?: boolean;
|
|
52
|
+
alreadyConnected?: boolean;
|
|
53
|
+
/** Snapshot WhatsApp (legacy, mantenido por compat con la tool de WA). */
|
|
54
|
+
connectedNumbers?: Array<{ id?: string; number?: string; alias?: string }>;
|
|
55
|
+
/** Snapshot IG/Messenger (pageId + alias). */
|
|
56
|
+
connectedItems?: Array<{ id?: string; pageId?: string; alias?: string }>;
|
|
57
|
+
existingIntegrationIds?: string[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type ConnectStatus =
|
|
61
|
+
| 'waiting' // card visible, polling activo
|
|
62
|
+
| 'activating' // integracion detectada, activando via API
|
|
63
|
+
| 'connected' // activada con exito
|
|
64
|
+
| 'timeout' // intentos agotados
|
|
65
|
+
| 'cancelled' // usuario cancelo
|
|
66
|
+
| 'error'; // activacion fallo
|
|
67
|
+
|
|
68
|
+
export interface ConnectState {
|
|
69
|
+
/** Id estable para keys/diffs. */
|
|
70
|
+
id: string;
|
|
71
|
+
channel: ConnectChannel;
|
|
72
|
+
linkData: ChannelLinkData;
|
|
73
|
+
status: ConnectStatus;
|
|
74
|
+
/** Mensaje del chat al que ancla la tarjeta. */
|
|
75
|
+
anchorMessageId: string;
|
|
76
|
+
/** Etiqueta humana del item conectado (numero, pagina, etc.) — populado en `connected`. */
|
|
77
|
+
connectedLabel?: string;
|
|
78
|
+
errorMessage?: string;
|
|
79
|
+
attempt: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Shape minimo de un item de `workspace.integrations` que devuelve el GET. */
|
|
83
|
+
export interface WorkspaceIntegrationItem {
|
|
84
|
+
id?: string;
|
|
85
|
+
type?: string;
|
|
86
|
+
/** whatsapp */
|
|
87
|
+
cellphoneNumber?: string;
|
|
88
|
+
cellphoneNumberFormat?: string;
|
|
89
|
+
/** facebook (messenger) */
|
|
90
|
+
facebookPageId?: string;
|
|
91
|
+
/** instagram */
|
|
92
|
+
instagramPageId?: string;
|
|
93
|
+
/** comun: alias humano */
|
|
94
|
+
aliasName?: string;
|
|
95
|
+
creationDate?: string;
|
|
96
|
+
isActive?: number | boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export const CONNECT_POLL_INTERVAL_MS = 5000;
|
|
100
|
+
export const CONNECT_POLL_MAX_ATTEMPTS = 60; // 5 minutos
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Devuelve la integracion mas reciente del canal cuyo id NO este en el baseline.
|
|
104
|
+
* Si hay varias nuevas, gana la de `creationDate` mayor.
|
|
105
|
+
*/
|
|
106
|
+
export function findNewIntegrationForChannel(
|
|
107
|
+
channel: ConnectChannel,
|
|
108
|
+
integrations: WorkspaceIntegrationItem[] | undefined,
|
|
109
|
+
knownIds: Set<string>,
|
|
110
|
+
): WorkspaceIntegrationItem | null {
|
|
111
|
+
if (!integrations?.length) return null;
|
|
112
|
+
|
|
113
|
+
const types = new Set(integrationTypesForChannel(channel));
|
|
114
|
+
const candidates = integrations.filter(
|
|
115
|
+
(i) => !!i.type && types.has(i.type) && !!i.id && !knownIds.has(i.id as string),
|
|
116
|
+
);
|
|
117
|
+
if (!candidates.length) return null;
|
|
118
|
+
|
|
119
|
+
return candidates.reduce((latest, current) => {
|
|
120
|
+
const a = latest.creationDate ? new Date(latest.creationDate).getTime() : 0;
|
|
121
|
+
const b = current.creationDate ? new Date(current.creationDate).getTime() : 0;
|
|
122
|
+
return b > a ? current : latest;
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Etiqueta humana del item recien conectado (numero, alias de pagina, etc.). */
|
|
127
|
+
export function describeIntegration(
|
|
128
|
+
channel: ConnectChannel,
|
|
129
|
+
i: WorkspaceIntegrationItem,
|
|
130
|
+
): string {
|
|
131
|
+
switch (channel) {
|
|
132
|
+
case 'whatsapp':
|
|
133
|
+
return i.cellphoneNumberFormat || i.cellphoneNumber || '';
|
|
134
|
+
case 'instagram':
|
|
135
|
+
return i.aliasName || i.instagramPageId || '';
|
|
136
|
+
case 'messenger':
|
|
137
|
+
return i.aliasName || i.facebookPageId || '';
|
|
138
|
+
}
|
|
139
|
+
}
|
package/src/studio/runRepl.tsx
CHANGED
|
@@ -15,12 +15,11 @@ interface RunReplOptions {
|
|
|
15
15
|
workspaceOverride?: string;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const SHOW_CURSOR = '\x1b[?25h';
|
|
18
|
+
// Estilo Claude Code: NO usamos alt screen buffer. Razon: en el alt screen el
|
|
19
|
+
// scrollback nativo de la terminal queda inaccesible y el usuario no puede
|
|
20
|
+
// revisar la conversacion anterior. Aceptamos el tradeoff de que en algunos
|
|
21
|
+
// resizes de la terminal pueda haber artifacts visuales temporales (los frames
|
|
22
|
+
// estaticos ya quedaron en scrollback gracias a <Static>).
|
|
24
23
|
|
|
25
24
|
export async function runRepl(opts: RunReplOptions): Promise<void> {
|
|
26
25
|
let creds;
|
|
@@ -49,32 +48,6 @@ export async function runRepl(opts: RunReplOptions): Promise<void> {
|
|
|
49
48
|
};
|
|
50
49
|
|
|
51
50
|
const version = readVersion();
|
|
52
|
-
const useAltScreen = !!process.stdout.isTTY;
|
|
53
|
-
|
|
54
|
-
// Cleanup idempotente; lo registramos en varias señales para que la terminal
|
|
55
|
-
// nunca quede en alt screen ni con el cursor oculto si el proceso muere mal.
|
|
56
|
-
let cleanedUp = false;
|
|
57
|
-
const restoreTerminal = () => {
|
|
58
|
-
if (cleanedUp) return;
|
|
59
|
-
cleanedUp = true;
|
|
60
|
-
if (useAltScreen) {
|
|
61
|
-
process.stdout.write(ALT_SCREEN_EXIT);
|
|
62
|
-
process.stdout.write(SHOW_CURSOR);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
if (useAltScreen) {
|
|
67
|
-
process.stdout.write(ALT_SCREEN_ENTER);
|
|
68
|
-
}
|
|
69
|
-
process.on('exit', restoreTerminal);
|
|
70
|
-
process.on('SIGHUP', restoreTerminal);
|
|
71
|
-
process.on('SIGTERM', restoreTerminal);
|
|
72
|
-
process.on('uncaughtException', (err) => {
|
|
73
|
-
restoreTerminal();
|
|
74
|
-
// Re-emit so node prints the trace after restoring the terminal.
|
|
75
|
-
console.error(err);
|
|
76
|
-
process.exit(1);
|
|
77
|
-
});
|
|
78
51
|
|
|
79
52
|
const { waitUntilExit } = render(
|
|
80
53
|
<App
|
|
@@ -94,11 +67,7 @@ export async function runRepl(opts: RunReplOptions): Promise<void> {
|
|
|
94
67
|
// no-op aquí; App.tsx llama exit() al detectar Ctrl+C.
|
|
95
68
|
});
|
|
96
69
|
|
|
97
|
-
|
|
98
|
-
await waitUntilExit();
|
|
99
|
-
} finally {
|
|
100
|
-
restoreTerminal();
|
|
101
|
-
}
|
|
70
|
+
await waitUntilExit();
|
|
102
71
|
}
|
|
103
72
|
|
|
104
73
|
function readVersion(): string {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ChannelLinkData,
|
|
5
|
+
ConnectChannel,
|
|
6
|
+
ConnectState,
|
|
7
|
+
ConnectStatus,
|
|
8
|
+
} from '../connect/types.js';
|
|
4
9
|
|
|
5
10
|
export type Role = 'user' | 'assistant' | 'system';
|
|
6
11
|
|
|
@@ -58,14 +63,14 @@ export interface StudioState {
|
|
|
58
63
|
setAbortController(ctrl: AbortController | null): void;
|
|
59
64
|
abortStream(): void;
|
|
60
65
|
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
/** Channel connection card state (whatsapp/instagram/messenger; one active at a time). */
|
|
67
|
+
connectCard: ConnectState | null;
|
|
68
|
+
startConnectCard(channel: ConnectChannel, linkData: ChannelLinkData, anchorMessageId: string): string;
|
|
69
|
+
updateConnectCard(patch: Partial<ConnectState>): void;
|
|
70
|
+
setConnectStatus(status: ConnectStatus, extra?: Partial<ConnectState>): void;
|
|
71
|
+
clearConnectCard(): void;
|
|
67
72
|
|
|
68
|
-
/** Borra mensajes/steps/
|
|
73
|
+
/** Borra mensajes/steps/connectCard pero MANTIENE agente cargado, usage y config. */
|
|
69
74
|
clearMessages(): void;
|
|
70
75
|
/** Marca que el próximo stream completo debe reemplazar el historial. */
|
|
71
76
|
setPendingCompact(v: boolean): void;
|
|
@@ -88,7 +93,7 @@ export const useStudioStore = create<StudioState>((set, get) => ({
|
|
|
88
93
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
89
94
|
streaming: false,
|
|
90
95
|
abortController: null,
|
|
91
|
-
|
|
96
|
+
connectCard: null,
|
|
92
97
|
pendingCompact: false,
|
|
93
98
|
|
|
94
99
|
pushUserMessage(content) {
|
|
@@ -178,11 +183,12 @@ export const useStudioStore = create<StudioState>((set, get) => ({
|
|
|
178
183
|
if (ctrl && !ctrl.signal.aborted) ctrl.abort();
|
|
179
184
|
},
|
|
180
185
|
|
|
181
|
-
|
|
186
|
+
startConnectCard(channel, linkData, anchorMessageId) {
|
|
182
187
|
const id = uid();
|
|
183
188
|
set({
|
|
184
|
-
|
|
189
|
+
connectCard: {
|
|
185
190
|
id,
|
|
191
|
+
channel,
|
|
186
192
|
linkData,
|
|
187
193
|
status: 'waiting',
|
|
188
194
|
anchorMessageId,
|
|
@@ -192,16 +198,16 @@ export const useStudioStore = create<StudioState>((set, get) => ({
|
|
|
192
198
|
return id;
|
|
193
199
|
},
|
|
194
200
|
|
|
195
|
-
|
|
196
|
-
set((s) => (s.
|
|
201
|
+
updateConnectCard(patch) {
|
|
202
|
+
set((s) => (s.connectCard ? { connectCard: { ...s.connectCard, ...patch } } : {}));
|
|
197
203
|
},
|
|
198
204
|
|
|
199
|
-
|
|
200
|
-
set((s) => (s.
|
|
205
|
+
setConnectStatus(status, extra) {
|
|
206
|
+
set((s) => (s.connectCard ? { connectCard: { ...s.connectCard, status, ...(extra ?? {}) } } : {}));
|
|
201
207
|
},
|
|
202
208
|
|
|
203
|
-
|
|
204
|
-
set({
|
|
209
|
+
clearConnectCard() {
|
|
210
|
+
set({ connectCard: null });
|
|
205
211
|
},
|
|
206
212
|
|
|
207
213
|
clearMessages() {
|
|
@@ -212,7 +218,7 @@ export const useStudioStore = create<StudioState>((set, get) => ({
|
|
|
212
218
|
steps: [],
|
|
213
219
|
streaming: false,
|
|
214
220
|
abortController: null,
|
|
215
|
-
|
|
221
|
+
connectCard: null,
|
|
216
222
|
pendingCompact: false,
|
|
217
223
|
// agentConfig, agentId, usage se mantienen para seguir trabajando.
|
|
218
224
|
});
|
|
@@ -238,7 +244,7 @@ export const useStudioStore = create<StudioState>((set, get) => ({
|
|
|
238
244
|
},
|
|
239
245
|
],
|
|
240
246
|
steps: [],
|
|
241
|
-
|
|
247
|
+
connectCard: null,
|
|
242
248
|
pendingCompact: false,
|
|
243
249
|
};
|
|
244
250
|
});
|
|
@@ -255,7 +261,7 @@ export const useStudioStore = create<StudioState>((set, get) => ({
|
|
|
255
261
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
256
262
|
streaming: false,
|
|
257
263
|
abortController: null,
|
|
258
|
-
|
|
264
|
+
connectCard: null,
|
|
259
265
|
pendingCompact: false,
|
|
260
266
|
});
|
|
261
267
|
},
|