plazbot-cli 0.3.2 → 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 +5 -0
- 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 -0
- package/src/studio/state/store.ts +26 -20
- package/dist/studio/components/WhatsappConnectCard.js +0 -57
- package/src/studio/whatsapp/types.ts +0 -80
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
/** Mapea el `type` del tool_result al canal logico del cliente. */
|
|
14
|
+
export function channelFromToolResultType(type) {
|
|
15
|
+
if (type === 'whatsapp_link')
|
|
16
|
+
return 'whatsapp';
|
|
17
|
+
if (type === 'instagram_link')
|
|
18
|
+
return 'instagram';
|
|
19
|
+
if (type === 'messenger_link')
|
|
20
|
+
return 'messenger';
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
/** Tipos de integracion (workspace.integrations[].type) a vigilar por canal. */
|
|
24
|
+
export function integrationTypesForChannel(channel) {
|
|
25
|
+
switch (channel) {
|
|
26
|
+
case 'whatsapp': return ['whatsapp', 'whatsapp_business'];
|
|
27
|
+
case 'instagram': return ['instagram'];
|
|
28
|
+
case 'messenger': return ['facebook']; // Messenger se guarda como type=facebook
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `platformCode` que espera POST .../activate. Para WhatsApp el endpoint usa
|
|
33
|
+
* este valor para una validacion de duplicados; para IG/Messenger no ejecuta
|
|
34
|
+
* ninguna logica especifica pero debe enviarse algo coherente.
|
|
35
|
+
*/
|
|
36
|
+
export function platformCodeForChannel(channel) {
|
|
37
|
+
switch (channel) {
|
|
38
|
+
case 'whatsapp': return 'whatsapp';
|
|
39
|
+
case 'instagram': return 'instagram';
|
|
40
|
+
case 'messenger': return 'facebook';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export const CONNECT_POLL_INTERVAL_MS = 5000;
|
|
44
|
+
export const CONNECT_POLL_MAX_ATTEMPTS = 60; // 5 minutos
|
|
45
|
+
/**
|
|
46
|
+
* Devuelve la integracion mas reciente del canal cuyo id NO este en el baseline.
|
|
47
|
+
* Si hay varias nuevas, gana la de `creationDate` mayor.
|
|
48
|
+
*/
|
|
49
|
+
export function findNewIntegrationForChannel(channel, integrations, knownIds) {
|
|
50
|
+
if (!integrations?.length)
|
|
51
|
+
return null;
|
|
52
|
+
const types = new Set(integrationTypesForChannel(channel));
|
|
53
|
+
const candidates = integrations.filter((i) => !!i.type && types.has(i.type) && !!i.id && !knownIds.has(i.id));
|
|
54
|
+
if (!candidates.length)
|
|
55
|
+
return null;
|
|
56
|
+
return candidates.reduce((latest, current) => {
|
|
57
|
+
const a = latest.creationDate ? new Date(latest.creationDate).getTime() : 0;
|
|
58
|
+
const b = current.creationDate ? new Date(current.creationDate).getTime() : 0;
|
|
59
|
+
return b > a ? current : latest;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/** Etiqueta humana del item recien conectado (numero, alias de pagina, etc.). */
|
|
63
|
+
export function describeIntegration(channel, i) {
|
|
64
|
+
switch (channel) {
|
|
65
|
+
case 'whatsapp':
|
|
66
|
+
return i.cellphoneNumberFormat || i.cellphoneNumber || '';
|
|
67
|
+
case 'instagram':
|
|
68
|
+
return i.aliasName || i.instagramPageId || '';
|
|
69
|
+
case 'messenger':
|
|
70
|
+
return i.aliasName || i.facebookPageId || '';
|
|
71
|
+
}
|
|
72
|
+
}
|
package/dist/studio/runRepl.js
CHANGED
|
@@ -6,6 +6,11 @@ import { dirname, join } from 'node:path';
|
|
|
6
6
|
import { App } from './components/App.js';
|
|
7
7
|
import { getStoredCredentials } from '../utils/credentials.js';
|
|
8
8
|
import { logger } from '../utils/logger.js';
|
|
9
|
+
// Estilo Claude Code: NO usamos alt screen buffer. Razon: en el alt screen el
|
|
10
|
+
// scrollback nativo de la terminal queda inaccesible y el usuario no puede
|
|
11
|
+
// revisar la conversacion anterior. Aceptamos el tradeoff de que en algunos
|
|
12
|
+
// resizes de la terminal pueda haber artifacts visuales temporales (los frames
|
|
13
|
+
// estaticos ya quedaron en scrollback gracias a <Static>).
|
|
9
14
|
export async function runRepl(opts) {
|
|
10
15
|
let creds;
|
|
11
16
|
try {
|
|
@@ -17,7 +17,7 @@ export const useStudioStore = create((set, get) => ({
|
|
|
17
17
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
18
18
|
streaming: false,
|
|
19
19
|
abortController: null,
|
|
20
|
-
|
|
20
|
+
connectCard: null,
|
|
21
21
|
pendingCompact: false,
|
|
22
22
|
pushUserMessage(content) {
|
|
23
23
|
const id = uid();
|
|
@@ -92,11 +92,12 @@ export const useStudioStore = create((set, get) => ({
|
|
|
92
92
|
if (ctrl && !ctrl.signal.aborted)
|
|
93
93
|
ctrl.abort();
|
|
94
94
|
},
|
|
95
|
-
|
|
95
|
+
startConnectCard(channel, linkData, anchorMessageId) {
|
|
96
96
|
const id = uid();
|
|
97
97
|
set({
|
|
98
|
-
|
|
98
|
+
connectCard: {
|
|
99
99
|
id,
|
|
100
|
+
channel,
|
|
100
101
|
linkData,
|
|
101
102
|
status: 'waiting',
|
|
102
103
|
anchorMessageId,
|
|
@@ -105,14 +106,14 @@ export const useStudioStore = create((set, get) => ({
|
|
|
105
106
|
});
|
|
106
107
|
return id;
|
|
107
108
|
},
|
|
108
|
-
|
|
109
|
-
set((s) => (s.
|
|
109
|
+
updateConnectCard(patch) {
|
|
110
|
+
set((s) => (s.connectCard ? { connectCard: { ...s.connectCard, ...patch } } : {}));
|
|
110
111
|
},
|
|
111
|
-
|
|
112
|
-
set((s) => (s.
|
|
112
|
+
setConnectStatus(status, extra) {
|
|
113
|
+
set((s) => (s.connectCard ? { connectCard: { ...s.connectCard, status, ...(extra ?? {}) } } : {}));
|
|
113
114
|
},
|
|
114
|
-
|
|
115
|
-
set({
|
|
115
|
+
clearConnectCard() {
|
|
116
|
+
set({ connectCard: null });
|
|
116
117
|
},
|
|
117
118
|
clearMessages() {
|
|
118
119
|
const ctrl = get().abortController;
|
|
@@ -123,7 +124,7 @@ export const useStudioStore = create((set, get) => ({
|
|
|
123
124
|
steps: [],
|
|
124
125
|
streaming: false,
|
|
125
126
|
abortController: null,
|
|
126
|
-
|
|
127
|
+
connectCard: null,
|
|
127
128
|
pendingCompact: false,
|
|
128
129
|
// agentConfig, agentId, usage se mantienen para seguir trabajando.
|
|
129
130
|
});
|
|
@@ -147,7 +148,7 @@ export const useStudioStore = create((set, get) => ({
|
|
|
147
148
|
},
|
|
148
149
|
],
|
|
149
150
|
steps: [],
|
|
150
|
-
|
|
151
|
+
connectCard: null,
|
|
151
152
|
pendingCompact: false,
|
|
152
153
|
};
|
|
153
154
|
});
|
|
@@ -164,7 +165,7 @@ export const useStudioStore = create((set, get) => ({
|
|
|
164
165
|
usage: { inputTokens: 0, outputTokens: 0 },
|
|
165
166
|
streaming: false,
|
|
166
167
|
abortController: null,
|
|
167
|
-
|
|
168
|
+
connectCard: null,
|
|
168
169
|
pendingCompact: false,
|
|
169
170
|
});
|
|
170
171
|
},
|
package/package.json
CHANGED
|
@@ -12,9 +12,9 @@ import { slashRegistry } from '../slash/registry.js';
|
|
|
12
12
|
import { streamStudio } from '../api/sseClient.js';
|
|
13
13
|
import { StudioHttpError } from '../api/types.js';
|
|
14
14
|
import type { StudioChunk, StudioStreamOptions } from '../api/types.js';
|
|
15
|
-
import {
|
|
16
|
-
import { workspaceOptsFromStream } from '../
|
|
17
|
-
import type
|
|
15
|
+
import { startConnectPolling, type PollingHandle } from '../connect/polling.js';
|
|
16
|
+
import { workspaceOptsFromStream } from '../connect/api.js';
|
|
17
|
+
import { channelFromToolResultType, type ChannelLinkData } from '../connect/types.js';
|
|
18
18
|
|
|
19
19
|
export interface AppProps {
|
|
20
20
|
version: string;
|
|
@@ -31,18 +31,18 @@ export function App({ version, stream, initialAgentId, dev, supportMode }: AppPr
|
|
|
31
31
|
const streaming = useStudioStore((s) => s.streaming);
|
|
32
32
|
const agentId = useStudioStore((s) => s.agentId);
|
|
33
33
|
|
|
34
|
-
// Active
|
|
35
|
-
const
|
|
34
|
+
// Active channel-connect polling handle (cancelled on Esc / unmount / new stream).
|
|
35
|
+
const connectPollingRef = useRef<PollingHandle | null>(null);
|
|
36
36
|
const streamingRef = useRef(streaming);
|
|
37
37
|
useEffect(() => { streamingRef.current = streaming; }, [streaming]);
|
|
38
38
|
// Ref so the polling onConnected callback can trigger sendToBackend from the
|
|
39
39
|
// most recent closure without resubscribing every render.
|
|
40
40
|
const sendBackendRef = useRef<(text: string) => Promise<void>>(async () => {});
|
|
41
41
|
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const stopConnectPolling = useCallback(() => {
|
|
43
|
+
if (connectPollingRef.current) {
|
|
44
|
+
connectPollingRef.current.cancel();
|
|
45
|
+
connectPollingRef.current = null;
|
|
46
46
|
}
|
|
47
47
|
}, []);
|
|
48
48
|
|
|
@@ -56,23 +56,23 @@ export function App({ version, stream, initialAgentId, dev, supportMode }: AppPr
|
|
|
56
56
|
|
|
57
57
|
// Cleanup on unmount.
|
|
58
58
|
useEffect(() => {
|
|
59
|
-
return () => {
|
|
60
|
-
}, [
|
|
59
|
+
return () => { stopConnectPolling(); };
|
|
60
|
+
}, [stopConnectPolling]);
|
|
61
61
|
|
|
62
62
|
// Atajos de teclado globales.
|
|
63
63
|
useInput((input, key) => {
|
|
64
64
|
if (key.escape) {
|
|
65
|
-
const
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
useStudioStore.getState().
|
|
65
|
+
const cc = useStudioStore.getState().connectCard;
|
|
66
|
+
if (cc && (cc.status === 'waiting' || cc.status === 'activating')) {
|
|
67
|
+
stopConnectPolling();
|
|
68
|
+
useStudioStore.getState().setConnectStatus('cancelled');
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
useStudioStore.getState().abortStream();
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
74
|
if (key.ctrl && input === 'c') {
|
|
75
|
-
|
|
75
|
+
stopConnectPolling();
|
|
76
76
|
useStudioStore.getState().abortStream();
|
|
77
77
|
exit();
|
|
78
78
|
return;
|
|
@@ -116,41 +116,49 @@ export function App({ version, stream, initialAgentId, dev, supportMode }: AppPr
|
|
|
116
116
|
if (typeof idCandidate === 'string') s.setAgentId(idCandidate);
|
|
117
117
|
}
|
|
118
118
|
}
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
// Channel connect tool (whatsapp / instagram / messenger): spin up the
|
|
120
|
+
// card + polling. The chunk discriminator (`whatsapp_link`,
|
|
121
|
+
// `instagram_link`, `messenger_link`) tells us which channel.
|
|
122
|
+
const channel = channelFromToolResultType(tr?.type);
|
|
123
|
+
if (channel && data) {
|
|
124
|
+
const linkData = data as ChannelLinkData;
|
|
122
125
|
// No link to show (e.g. alreadyConnected without a new url) → skip card.
|
|
123
126
|
if (linkData.shortUrl) {
|
|
124
127
|
// Cancel any previous active polling before starting a new one.
|
|
125
|
-
|
|
126
|
-
s.
|
|
127
|
-
|
|
128
|
+
stopConnectPolling();
|
|
129
|
+
s.startConnectCard(channel, linkData, assistantId);
|
|
130
|
+
connectPollingRef.current = startConnectPolling(
|
|
128
131
|
workspaceOptsFromStream(stream),
|
|
132
|
+
channel,
|
|
129
133
|
linkData,
|
|
130
134
|
{
|
|
131
135
|
onAttempt(attempt) {
|
|
132
|
-
useStudioStore.getState().
|
|
136
|
+
useStudioStore.getState().updateConnectCard({ attempt });
|
|
133
137
|
},
|
|
134
138
|
onActivating() {
|
|
135
|
-
useStudioStore.getState().
|
|
139
|
+
useStudioStore.getState().setConnectStatus('activating');
|
|
136
140
|
},
|
|
137
|
-
onConnected(
|
|
138
|
-
useStudioStore.getState().
|
|
139
|
-
|
|
141
|
+
onConnected(label) {
|
|
142
|
+
useStudioStore.getState().setConnectStatus('connected', { connectedLabel: label });
|
|
143
|
+
connectPollingRef.current = null;
|
|
140
144
|
if (!streamingRef.current) {
|
|
145
|
+
const channelName =
|
|
146
|
+
channel === 'whatsapp' ? 'WhatsApp'
|
|
147
|
+
: channel === 'instagram' ? 'Instagram'
|
|
148
|
+
: 'Messenger';
|
|
141
149
|
const followUp =
|
|
142
|
-
`My
|
|
150
|
+
`My ${channelName} is now connected. ${label ? `Label: ${label}.` : ''} ` +
|
|
143
151
|
`Continue with the agent setup.`;
|
|
144
152
|
void sendBackendRef.current(followUp);
|
|
145
153
|
}
|
|
146
154
|
},
|
|
147
155
|
onTimeout() {
|
|
148
|
-
useStudioStore.getState().
|
|
149
|
-
|
|
156
|
+
useStudioStore.getState().setConnectStatus('timeout');
|
|
157
|
+
connectPollingRef.current = null;
|
|
150
158
|
},
|
|
151
159
|
onError(message) {
|
|
152
|
-
useStudioStore.getState().
|
|
153
|
-
|
|
160
|
+
useStudioStore.getState().setConnectStatus('error', { errorMessage: message });
|
|
161
|
+
connectPollingRef.current = null;
|
|
154
162
|
},
|
|
155
163
|
},
|
|
156
164
|
);
|
|
@@ -195,7 +203,7 @@ export function App({ version, stream, initialAgentId, dev, supportMode }: AppPr
|
|
|
195
203
|
useStudioStore.getState().compactInto(assistantId);
|
|
196
204
|
}
|
|
197
205
|
}
|
|
198
|
-
}, [stream, dev,
|
|
206
|
+
}, [stream, dev, stopConnectPolling]);
|
|
199
207
|
|
|
200
208
|
useEffect(() => { sendBackendRef.current = sendToBackend; }, [sendToBackend]);
|
|
201
209
|
|
|
@@ -2,33 +2,32 @@ import React from 'react';
|
|
|
2
2
|
import { Box, Static } from 'ink';
|
|
3
3
|
import { useStudioStore } from '../state/store.js';
|
|
4
4
|
import type { ChatMessage, ToolStep } from '../state/store.js';
|
|
5
|
+
import type { ConnectState } from '../connect/types.js';
|
|
5
6
|
import { Message } from './Message.js';
|
|
6
7
|
import { ToolCall } from './ToolCall.js';
|
|
7
|
-
import {
|
|
8
|
+
import { ConnectCard } from './ConnectCard.js';
|
|
8
9
|
|
|
9
10
|
type HistoricItem =
|
|
10
11
|
| { kind: 'message'; key: string; message: ChatMessage }
|
|
11
|
-
| { kind: 'step'; key: string; step: ToolStep }
|
|
12
|
+
| { kind: 'step'; key: string; step: ToolStep }
|
|
13
|
+
| { kind: 'connectCard'; key: string; state: ConnectState };
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
|
-
*
|
|
16
|
+
* Renderiza el log de la conversacion.
|
|
15
17
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* This is what fixes the "Header echoed multiple times" effect you see when
|
|
20
|
-
* dragging the terminal window while the dynamic frame is taller than the
|
|
21
|
-
* viewport.
|
|
18
|
+
* Items estables (mensajes finalizados + tool steps resueltos que no estan
|
|
19
|
+
* anclados a una tarjeta de conexion activa) van dentro de <Static> para que
|
|
20
|
+
* Ink los escriba al scrollback una sola vez y no los re-renderice en resize.
|
|
22
21
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
22
|
+
* El bloque dinamico al final contiene el mensaje en streaming, los steps que
|
|
23
|
+
* siguen en `running`, y la tarjeta de conexion (whatsapp/instagram/messenger)
|
|
24
|
+
* mientras esta activa.
|
|
25
25
|
*/
|
|
26
26
|
export function ChatLog(): React.ReactElement {
|
|
27
27
|
const messages = useStudioStore((s) => s.messages);
|
|
28
28
|
const steps = useStudioStore((s) => s.steps);
|
|
29
|
-
const
|
|
29
|
+
const connectCard = useStudioStore((s) => s.connectCard);
|
|
30
30
|
|
|
31
|
-
// Group steps by their anchor message for quick lookup.
|
|
32
31
|
const stepsByMsg = new Map<string, ToolStep[]>();
|
|
33
32
|
for (const step of steps) {
|
|
34
33
|
const arr = stepsByMsg.get(step.afterMessageId) ?? [];
|
|
@@ -36,27 +35,52 @@ export function ChatLog(): React.ReactElement {
|
|
|
36
35
|
stepsByMsg.set(step.afterMessageId, arr);
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
38
|
+
// La tarjeta solo permanece en el dynamic frame mientras esta "viva"
|
|
39
|
+
// (polling activo o activando). En estados terminales
|
|
40
|
+
// (connected/cancelled/timeout/error) migra a <Static> junto a su mensaje
|
|
41
|
+
// ancla, de modo que los mensajes nuevos fluyen debajo.
|
|
42
|
+
const cardActive =
|
|
43
|
+
!!connectCard && (connectCard.status === 'waiting' || connectCard.status === 'activating');
|
|
44
|
+
const cardInScrollback = !!connectCard && !cardActive;
|
|
45
|
+
|
|
46
|
+
// Mensajes que deben quedar en el dynamic frame:
|
|
47
|
+
// - el ultimo si sigue streameando
|
|
48
|
+
// - cualquiera con tool steps en running
|
|
49
|
+
// - el ancla de una tarjeta de conexion ACTIVA
|
|
43
50
|
const dynamicMsgIds = new Set<string>();
|
|
44
51
|
const last = messages[messages.length - 1];
|
|
45
52
|
if (last?.streaming) dynamicMsgIds.add(last.id);
|
|
46
53
|
for (const step of steps) {
|
|
47
54
|
if (step.status === 'running') dynamicMsgIds.add(step.afterMessageId);
|
|
48
55
|
}
|
|
49
|
-
if (
|
|
56
|
+
if (cardActive && connectCard) dynamicMsgIds.add(connectCard.anchorMessageId);
|
|
50
57
|
|
|
51
|
-
// Build the static (scrollback) list. Each message and step gets a unique
|
|
52
|
-
// key so React/Ink only writes it once.
|
|
53
58
|
const staticItems: HistoricItem[] = [];
|
|
59
|
+
let cardInjected = false;
|
|
54
60
|
for (const m of messages) {
|
|
55
61
|
if (dynamicMsgIds.has(m.id)) continue;
|
|
56
62
|
staticItems.push({ kind: 'message', key: `m-${m.id}`, message: m });
|
|
57
63
|
for (const s of stepsByMsg.get(m.id) ?? []) {
|
|
58
64
|
staticItems.push({ kind: 'step', key: `s-${s.id}`, step: s });
|
|
59
65
|
}
|
|
66
|
+
if (cardInScrollback && connectCard && connectCard.anchorMessageId === m.id) {
|
|
67
|
+
staticItems.push({
|
|
68
|
+
kind: 'connectCard',
|
|
69
|
+
key: `cc-${connectCard.id}-${connectCard.status}`,
|
|
70
|
+
state: connectCard,
|
|
71
|
+
});
|
|
72
|
+
cardInjected = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Fallback: la tarjeta terminal quedo huerfana porque su mensaje ancla ya no
|
|
76
|
+
// esta (p.ej. tras /compact). La empujamos igual al scrollback para no perder
|
|
77
|
+
// el registro visual.
|
|
78
|
+
if (cardInScrollback && connectCard && !cardInjected) {
|
|
79
|
+
staticItems.push({
|
|
80
|
+
kind: 'connectCard',
|
|
81
|
+
key: `cc-${connectCard.id}-${connectCard.status}-orphan`,
|
|
82
|
+
state: connectCard,
|
|
83
|
+
});
|
|
60
84
|
}
|
|
61
85
|
|
|
62
86
|
const dynamicMessages = messages.filter((m) => dynamicMsgIds.has(m.id));
|
|
@@ -64,13 +88,15 @@ export function ChatLog(): React.ReactElement {
|
|
|
64
88
|
return (
|
|
65
89
|
<Box flexDirection="column">
|
|
66
90
|
<Static items={staticItems}>
|
|
67
|
-
{(item) =>
|
|
68
|
-
item.kind === 'message'
|
|
69
|
-
<Message key={item.key} message={item.message}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
91
|
+
{(item) => {
|
|
92
|
+
if (item.kind === 'message') {
|
|
93
|
+
return <Message key={item.key} message={item.message} />;
|
|
94
|
+
}
|
|
95
|
+
if (item.kind === 'step') {
|
|
96
|
+
return <ToolCall key={item.key} step={item.step} />;
|
|
97
|
+
}
|
|
98
|
+
return <ConnectCard key={item.key} state={item.state} />;
|
|
99
|
+
}}
|
|
74
100
|
</Static>
|
|
75
101
|
|
|
76
102
|
<Box flexDirection="column">
|
|
@@ -80,14 +106,14 @@ export function ChatLog(): React.ReactElement {
|
|
|
80
106
|
{(stepsByMsg.get(m.id) ?? []).map((s) => (
|
|
81
107
|
<ToolCall key={s.id} step={s} />
|
|
82
108
|
))}
|
|
83
|
-
{
|
|
84
|
-
<
|
|
109
|
+
{cardActive && connectCard && connectCard.anchorMessageId === m.id ? (
|
|
110
|
+
<ConnectCard state={connectCard} />
|
|
85
111
|
) : null}
|
|
86
112
|
</React.Fragment>
|
|
87
113
|
))}
|
|
88
|
-
{/* Fallback:
|
|
89
|
-
{
|
|
90
|
-
<
|
|
114
|
+
{/* Fallback: tarjeta activa cuyo ancla ya no esta en messages. */}
|
|
115
|
+
{cardActive && connectCard && !messages.some((m) => m.id === connectCard.anchorMessageId) ? (
|
|
116
|
+
<ConnectCard state={connectCard} />
|
|
91
117
|
) : null}
|
|
92
118
|
</Box>
|
|
93
119
|
</Box>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import Spinner from 'ink-spinner';
|
|
4
|
-
import type {
|
|
5
|
-
import {
|
|
4
|
+
import type { ConnectChannel, ConnectState } from '../connect/types.js';
|
|
5
|
+
import { CONNECT_POLL_MAX_ATTEMPTS } from '../connect/types.js';
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
|
-
state:
|
|
8
|
+
state: ConnectState;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function formatExpiry(iso?: string): string {
|
|
@@ -21,33 +21,69 @@ function formatExpiry(iso?: string): string {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Envuelve una URL en un hyperlink OSC 8. Terminales que no lo soportan
|
|
25
|
+
* imprimen solo el label visible — iTerm2, kitty, Wezterm, Alacritty (reciente),
|
|
26
|
+
* VS Code/Hyper modernos si lo soportan.
|
|
27
27
|
*/
|
|
28
28
|
function osc8(url: string, label: string): string {
|
|
29
29
|
return `\u001b]8;;${url}\u001b\\${label}\u001b]8;;\u001b\\`;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
interface ChannelCopy {
|
|
33
|
+
headerWaiting: string;
|
|
34
|
+
headerConnected: string;
|
|
35
|
+
typeLabel: (linkType: string | undefined) => string;
|
|
36
|
+
hint: string;
|
|
37
|
+
connectedTpl: (label: string) => string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function copyFor(channel: ConnectChannel): ChannelCopy {
|
|
41
|
+
switch (channel) {
|
|
42
|
+
case 'whatsapp':
|
|
43
|
+
return {
|
|
44
|
+
headerWaiting: 'Connect your WhatsApp',
|
|
45
|
+
headerConnected: 'WhatsApp connected',
|
|
46
|
+
typeLabel: (lt) => (lt === 'whatsapp_business' ? 'WhatsApp Business' : 'WhatsApp Cloud API'),
|
|
47
|
+
hint: "Open the link from a phone with WhatsApp installed. We'll keep watching for the new number.",
|
|
48
|
+
connectedTpl: (n) => `Number ${n || '(connected)'} activated successfully.`,
|
|
49
|
+
};
|
|
50
|
+
case 'instagram':
|
|
51
|
+
return {
|
|
52
|
+
headerWaiting: 'Connect your Instagram',
|
|
53
|
+
headerConnected: 'Instagram connected',
|
|
54
|
+
typeLabel: () => 'Instagram Business',
|
|
55
|
+
hint: "Open the link in your browser and authorize the Instagram Business account linked to a Facebook Page. We'll keep watching for the connection.",
|
|
56
|
+
connectedTpl: (n) => `Account ${n || '(connected)'} activated successfully.`,
|
|
57
|
+
};
|
|
58
|
+
case 'messenger':
|
|
59
|
+
return {
|
|
60
|
+
headerWaiting: 'Connect your Messenger',
|
|
61
|
+
headerConnected: 'Messenger connected',
|
|
62
|
+
typeLabel: () => 'Facebook Page',
|
|
63
|
+
hint: "Open the link in your browser and select the Facebook Page(s) you want to use with Messenger. We'll keep watching for the connection.",
|
|
64
|
+
connectedTpl: (n) => `Page ${n || '(connected)'} activated successfully.`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function ConnectCard({ state }: Props): React.ReactElement {
|
|
70
|
+
const { channel, linkData, status, connectedLabel, errorMessage, attempt } = state;
|
|
34
71
|
const shortUrl = linkData.shortUrl || '';
|
|
35
72
|
const expiry = formatExpiry(linkData.expiresAt);
|
|
36
|
-
const
|
|
37
|
-
|
|
73
|
+
const copy = copyFor(channel);
|
|
74
|
+
const typeLabel = copy.typeLabel(linkData.linkType);
|
|
38
75
|
|
|
39
|
-
// Header glyph + color per status.
|
|
40
76
|
let headerIcon = '●';
|
|
41
77
|
let headerColor: 'green' | 'red' | 'yellow' = 'green';
|
|
42
|
-
let headerText =
|
|
78
|
+
let headerText = copy.headerWaiting;
|
|
43
79
|
if (status === 'connected') {
|
|
44
80
|
headerIcon = '✓';
|
|
45
81
|
headerColor = 'green';
|
|
46
|
-
headerText =
|
|
82
|
+
headerText = copy.headerConnected;
|
|
47
83
|
} else if (status === 'error') {
|
|
48
84
|
headerIcon = '✖';
|
|
49
85
|
headerColor = 'red';
|
|
50
|
-
headerText = 'Could not activate the
|
|
86
|
+
headerText = 'Could not activate the integration';
|
|
51
87
|
} else if (status === 'timeout') {
|
|
52
88
|
headerIcon = '○';
|
|
53
89
|
headerColor = 'yellow';
|
|
@@ -94,9 +130,7 @@ export function WhatsappConnectCard({ state }: Props): React.ReactElement {
|
|
|
94
130
|
</Box>
|
|
95
131
|
) : null}
|
|
96
132
|
<Box marginTop={1}>
|
|
97
|
-
<Text dimColor>
|
|
98
|
-
Open the link from a phone with WhatsApp installed. We'll keep watching for the new number.
|
|
99
|
-
</Text>
|
|
133
|
+
<Text dimColor>{copy.hint}</Text>
|
|
100
134
|
</Box>
|
|
101
135
|
</Box>
|
|
102
136
|
) : null}
|
|
@@ -107,20 +141,18 @@ export function WhatsappConnectCard({ state }: Props): React.ReactElement {
|
|
|
107
141
|
<>
|
|
108
142
|
<Text color="yellow"><Spinner type="dots" /></Text>
|
|
109
143
|
<Text dimColor> Waiting for connection… </Text>
|
|
110
|
-
<Text dimColor>({attempt}/{
|
|
144
|
+
<Text dimColor>({attempt}/{CONNECT_POLL_MAX_ATTEMPTS})</Text>
|
|
111
145
|
<Text dimColor> · press Esc to cancel</Text>
|
|
112
146
|
</>
|
|
113
147
|
) : null}
|
|
114
148
|
{status === 'activating' ? (
|
|
115
149
|
<>
|
|
116
150
|
<Text color="yellow"><Spinner type="dots" /></Text>
|
|
117
|
-
<Text dimColor>
|
|
151
|
+
<Text dimColor> Connection detected, activating…</Text>
|
|
118
152
|
</>
|
|
119
153
|
) : null}
|
|
120
154
|
{status === 'connected' ? (
|
|
121
|
-
<Text color="green">
|
|
122
|
-
Number {connectedNumber || '(connected)'} activated successfully.
|
|
123
|
-
</Text>
|
|
155
|
+
<Text color="green">{copy.connectedTpl(connectedLabel || '')}</Text>
|
|
124
156
|
) : null}
|
|
125
157
|
{status === 'timeout' ? (
|
|
126
158
|
<Text dimColor>
|
|
@@ -31,7 +31,7 @@ export function Footer({ inputTokens, outputTokens, streaming }: FooterProps): R
|
|
|
31
31
|
) : null}
|
|
32
32
|
</Box>
|
|
33
33
|
<Box>
|
|
34
|
-
<Text dimColor>/help · Esc cancel · Ctrl+C quit</Text>
|
|
34
|
+
<Text dimColor>/help · ↑↓ history · Tab accept · Esc cancel · Ctrl+C quit</Text>
|
|
35
35
|
</Box>
|
|
36
36
|
</Box>
|
|
37
37
|
);
|