plazbot-cli 0.3.7 → 0.4.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 (116) hide show
  1. package/README.md +38 -2
  2. package/dist/commands/auth/browser-login.js +121 -0
  3. package/dist/commands/auth/index.js +5 -1
  4. package/dist/commands/auth/login.js +1 -3
  5. package/dist/commands/auth/logout.js +4 -3
  6. package/dist/commands/auth/status.js +8 -5
  7. package/dist/commands/auth/workspace.js +73 -0
  8. package/dist/commands/whatsapp/connect-flow.js +137 -0
  9. package/dist/commands/whatsapp/connect.js +26 -160
  10. package/dist/commands/whatsapp/index.js +4 -0
  11. package/dist/commands/whatsapp/setup.js +242 -0
  12. package/dist/studio/components/App.js +1 -1
  13. package/dist/studio/runOneShot.js +2 -2
  14. package/dist/studio/runRepl.js +1 -1
  15. package/dist/utils/agent-errors.js +2 -2
  16. package/dist/utils/banner.js +10 -9
  17. package/dist/utils/credentials.js +31 -3
  18. package/dist/utils/open-browser.js +17 -0
  19. package/package.json +14 -6
  20. package/.claude/settings.local.json +0 -19
  21. package/CLAUDE.md +0 -121
  22. package/agent.config.json +0 -21
  23. package/dist/studio/components/AgentPanel.js +0 -35
  24. package/dist/studio/render/json.js +0 -16
  25. package/dist/studio/whatsapp/api.js +0 -63
  26. package/dist/studio/whatsapp/polling.js +0 -77
  27. package/dist/studio/whatsapp/types.js +0 -31
  28. package/src/cli.ts +0 -57
  29. package/src/commands/agent/ai-config.ts +0 -164
  30. package/src/commands/agent/chat.ts +0 -225
  31. package/src/commands/agent/copy.ts +0 -46
  32. package/src/commands/agent/create.ts +0 -103
  33. package/src/commands/agent/delete.ts +0 -71
  34. package/src/commands/agent/enable-widget.ts +0 -67
  35. package/src/commands/agent/export.ts +0 -430
  36. package/src/commands/agent/files.ts +0 -171
  37. package/src/commands/agent/get.ts +0 -233
  38. package/src/commands/agent/index.ts +0 -55
  39. package/src/commands/agent/list.ts +0 -58
  40. package/src/commands/agent/monitor.ts +0 -375
  41. package/src/commands/agent/on-message.ts +0 -82
  42. package/src/commands/agent/set.ts +0 -149
  43. package/src/commands/agent/templates.ts +0 -892
  44. package/src/commands/agent/tools.ts +0 -168
  45. package/src/commands/agent/update.ts +0 -61
  46. package/src/commands/agent/validate.ts +0 -135
  47. package/src/commands/agent/wizard.ts +0 -756
  48. package/src/commands/auth/index.ts +0 -10
  49. package/src/commands/auth/login.ts +0 -72
  50. package/src/commands/auth/logout.ts +0 -28
  51. package/src/commands/auth/status.ts +0 -28
  52. package/src/commands/portal/add-agent.ts +0 -66
  53. package/src/commands/portal/add-link.ts +0 -47
  54. package/src/commands/portal/clear-links.ts +0 -33
  55. package/src/commands/portal/create.ts +0 -58
  56. package/src/commands/portal/delete.ts +0 -70
  57. package/src/commands/portal/get.ts +0 -77
  58. package/src/commands/portal/index.ts +0 -32
  59. package/src/commands/portal/list.ts +0 -79
  60. package/src/commands/portal/update.ts +0 -93
  61. package/src/commands/whatsapp/broadcast.ts +0 -110
  62. package/src/commands/whatsapp/channels.ts +0 -109
  63. package/src/commands/whatsapp/chat.ts +0 -85
  64. package/src/commands/whatsapp/connect.ts +0 -220
  65. package/src/commands/whatsapp/delete-webhook.ts +0 -43
  66. package/src/commands/whatsapp/index.ts +0 -37
  67. package/src/commands/whatsapp/register-webhook.ts +0 -47
  68. package/src/commands/whatsapp/send-template.ts +0 -94
  69. package/src/commands/whatsapp/send.ts +0 -55
  70. package/src/commands/whatsapp/widget.ts +0 -75
  71. package/src/commands/workers/deploy.ts +0 -156
  72. package/src/commands/workers/index.ts +0 -30
  73. package/src/commands/workers/list.ts +0 -105
  74. package/src/commands/workers/logs.ts +0 -99
  75. package/src/commands/workers/remove.ts +0 -103
  76. package/src/commands/workers/secret.ts +0 -191
  77. package/src/commands/workers/test.ts +0 -145
  78. package/src/schemas/agent.config.schema.json +0 -569
  79. package/src/studio/api/sseClient.ts +0 -91
  80. package/src/studio/api/studioApi.ts +0 -27
  81. package/src/studio/api/types.ts +0 -96
  82. package/src/studio/components/App.tsx +0 -274
  83. package/src/studio/components/ChatLog.tsx +0 -121
  84. package/src/studio/components/ConnectCard.tsx +0 -171
  85. package/src/studio/components/Footer.tsx +0 -38
  86. package/src/studio/components/Header.tsx +0 -39
  87. package/src/studio/components/Input.tsx +0 -176
  88. package/src/studio/components/Message.tsx +0 -87
  89. package/src/studio/components/Suggestions.tsx +0 -26
  90. package/src/studio/components/ToolCall.tsx +0 -58
  91. package/src/studio/connect/api.ts +0 -98
  92. package/src/studio/connect/polling.ts +0 -94
  93. package/src/studio/connect/types.ts +0 -141
  94. package/src/studio/index.ts +0 -58
  95. package/src/studio/render/markdown.ts +0 -93
  96. package/src/studio/render/steps.ts +0 -57
  97. package/src/studio/runOneShot.ts +0 -114
  98. package/src/studio/runRepl.tsx +0 -82
  99. package/src/studio/slash/handlers.ts +0 -226
  100. package/src/studio/slash/parser.ts +0 -41
  101. package/src/studio/slash/registry.ts +0 -54
  102. package/src/studio/state/store.ts +0 -279
  103. package/src/types/agent.ts +0 -63
  104. package/src/types/auth.ts +0 -9
  105. package/src/types/common.ts +0 -10
  106. package/src/types/message.ts +0 -34
  107. package/src/types/portal.ts +0 -56
  108. package/src/types/workers.ts +0 -86
  109. package/src/utils/agent-errors.ts +0 -67
  110. package/src/utils/api.ts +0 -35
  111. package/src/utils/banner.ts +0 -99
  112. package/src/utils/credentials.ts +0 -38
  113. package/src/utils/help.ts +0 -51
  114. package/src/utils/logger.ts +0 -40
  115. package/src/utils/ui.ts +0 -109
  116. package/tsconfig.json +0 -18
@@ -1,27 +0,0 @@
1
- import type { StudioStreamOptions } from './types.js';
2
-
3
- /**
4
- * Construye la URL base según zona / modo dev.
5
- * LA -> https://api.plazbot.com
6
- * EU -> https://apieu.plazbot.com
7
- * dev -> http://localhost:5090
8
- */
9
- export function getBaseUrl(zone: 'LA' | 'EU', dev?: boolean): string {
10
- if (dev) return 'http://localhost:5090';
11
- return zone === 'EU' ? 'https://apieu.plazbot.com' : 'https://api.plazbot.com';
12
- }
13
-
14
- export function buildStudioUrl(zone: 'LA' | 'EU', dev?: boolean): string {
15
- return `${getBaseUrl(zone, dev)}/api/agent/studio`;
16
- }
17
-
18
- export function buildStudioHeaders(opts: StudioStreamOptions): Record<string, string> {
19
- const headers: Record<string, string> = {
20
- 'Content-Type': 'application/json',
21
- Accept: 'text/event-stream',
22
- Authorization: `Bearer ${opts.apiKey}`,
23
- 'x-workspace-id': opts.workspaceId,
24
- };
25
- if (opts.userId) headers['x-user-id'] = opts.userId;
26
- return headers;
27
- }
@@ -1,96 +0,0 @@
1
- /**
2
- * Tipos de los chunks que emite el endpoint POST /api/agent/studio (SSE).
3
- * Espejo del modelo del backend AgentStudioModels.cs.
4
- */
5
-
6
- export type StudioChunkType = 'text' | 'tool_call' | 'tool_result' | 'usage' | 'done' | 'error';
7
-
8
- export interface StudioChunkText {
9
- type: 'text';
10
- content: string;
11
- }
12
-
13
- export interface StudioChunkToolCall {
14
- type: 'tool_call';
15
- tool_name: string;
16
- tool_args?: unknown;
17
- }
18
-
19
- /** Resultado genérico devuelto por una tool (espejo de ToolExecutionResult.cs). */
20
- export interface ToolExecutionResult {
21
- success: boolean;
22
- tool?: string;
23
- type?: string; // "agent_config" | "agent_list" | "diagnostic" | "agent_loaded" | ...
24
- data?: unknown;
25
- message?: string;
26
- error?: string;
27
- }
28
-
29
- export interface StudioChunkToolResult {
30
- type: 'tool_result';
31
- tool_name: string;
32
- tool_result?: ToolExecutionResult;
33
- }
34
-
35
- export interface StudioChunkUsage {
36
- type: 'usage';
37
- input_tokens: number;
38
- output_tokens: number;
39
- }
40
-
41
- export interface StudioChunkDone {
42
- type: 'done';
43
- }
44
-
45
- export interface StudioChunkError {
46
- type: 'error';
47
- error: string;
48
- }
49
-
50
- export type StudioChunk =
51
- | StudioChunkText
52
- | StudioChunkToolCall
53
- | StudioChunkToolResult
54
- | StudioChunkUsage
55
- | StudioChunkDone
56
- | StudioChunkError;
57
-
58
- /**
59
- * Mensaje conversacional enviado al backend en el array `messages`.
60
- */
61
- export interface StudioMessageIn {
62
- role: 'user' | 'assistant';
63
- content: string;
64
- }
65
-
66
- /**
67
- * Payload del request POST /api/agent/studio.
68
- * NB: el backend valida `message` (singular, string) y devuelve 400 si está vacío.
69
- * `messages` lleva el historial; `message` lleva el último turno del usuario.
70
- */
71
- export interface StudioRequest {
72
- message: string;
73
- messages: StudioMessageIn[];
74
- agentId?: string | null;
75
- agentConfig?: unknown;
76
- workspaceId?: string;
77
- userId?: string;
78
- }
79
-
80
- /**
81
- * Opciones para el cliente SSE (autenticación + ruteo).
82
- */
83
- export interface StudioStreamOptions {
84
- apiKey: string; // JWT
85
- workspaceId: string;
86
- userId?: string;
87
- zone: 'LA' | 'EU';
88
- dev?: boolean;
89
- }
90
-
91
- export class StudioHttpError extends Error {
92
- constructor(public status: number, public statusText: string, public body?: string) {
93
- super(`HTTP ${status} ${statusText}`);
94
- this.name = 'StudioHttpError';
95
- }
96
- }
@@ -1,274 +0,0 @@
1
- import React, { useCallback, useEffect, useRef } from 'react';
2
- import { Box, useApp, useInput } from 'ink';
3
- import { Header } from './Header.js';
4
- import { Footer } from './Footer.js';
5
- import { ChatLog } from './ChatLog.js';
6
- import { Input } from './Input.js';
7
- import { Suggestions } from './Suggestions.js';
8
- import { useStudioStore, selectBackendMessages } from '../state/store.js';
9
- import { parseSlash } from '../slash/parser.js';
10
- import '../slash/handlers.js'; // side effect: registra slashes
11
- import { slashRegistry } from '../slash/registry.js';
12
- import { streamStudio } from '../api/sseClient.js';
13
- import { StudioHttpError } from '../api/types.js';
14
- import type { StudioChunk, StudioStreamOptions } from '../api/types.js';
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
-
19
- export interface AppProps {
20
- version: string;
21
- stream: StudioStreamOptions;
22
- initialAgentId?: string | null;
23
- dev: boolean;
24
- supportMode?: boolean;
25
- }
26
-
27
- export function App({ version, stream, initialAgentId, dev, supportMode }: AppProps): React.ReactElement {
28
- const { exit } = useApp();
29
- const messages = useStudioStore((s) => s.messages);
30
- const usage = useStudioStore((s) => s.usage);
31
- const streaming = useStudioStore((s) => s.streaming);
32
- const agentId = useStudioStore((s) => s.agentId);
33
-
34
- // Active channel-connect polling handle (cancelled on Esc / unmount / new stream).
35
- const connectPollingRef = useRef<PollingHandle | null>(null);
36
- const streamingRef = useRef(streaming);
37
- useEffect(() => { streamingRef.current = streaming; }, [streaming]);
38
- // Ref so the polling onConnected callback can trigger sendToBackend from the
39
- // most recent closure without resubscribing every render.
40
- const sendBackendRef = useRef<(text: string) => Promise<void>>(async () => {});
41
-
42
- const stopConnectPolling = useCallback(() => {
43
- if (connectPollingRef.current) {
44
- connectPollingRef.current.cancel();
45
- connectPollingRef.current = null;
46
- }
47
- }, []);
48
-
49
- // Cargar agente inicial si se pasó por flag.
50
- useEffect(() => {
51
- if (initialAgentId) {
52
- useStudioStore.getState().setAgentId(initialAgentId);
53
- }
54
- // eslint-disable-next-line react-hooks/exhaustive-deps
55
- }, []);
56
-
57
- // Cleanup on unmount.
58
- useEffect(() => {
59
- return () => { stopConnectPolling(); };
60
- }, [stopConnectPolling]);
61
-
62
- // Atajos de teclado globales.
63
- useInput((input, key) => {
64
- if (key.escape) {
65
- const cc = useStudioStore.getState().connectCard;
66
- if (cc && (cc.status === 'waiting' || cc.status === 'activating')) {
67
- stopConnectPolling();
68
- useStudioStore.getState().setConnectStatus('cancelled');
69
- return;
70
- }
71
- useStudioStore.getState().abortStream();
72
- return;
73
- }
74
- if (key.ctrl && input === 'c') {
75
- stopConnectPolling();
76
- useStudioStore.getState().abortStream();
77
- exit();
78
- return;
79
- }
80
- });
81
-
82
- const sendToBackend = useCallback(async (userText: string) => {
83
- const store = useStudioStore.getState();
84
- // Snapshot del historial ANTES de agregar el nuevo user message, igual que
85
- // el front (AgentStudioModal.tsx:504). Si se calcula después, el último
86
- // user input queda duplicado (en `message` y en `messages`).
87
- const historyForApi = selectBackendMessages();
88
- store.pushUserMessage(userText);
89
-
90
- const assistantId = store.startAssistantMessage();
91
- const controller = new AbortController();
92
- store.setAbortController(controller);
93
- store.setStreaming(true);
94
-
95
- const onChunk = (chunk: StudioChunk) => {
96
- const s = useStudioStore.getState();
97
- switch (chunk.type) {
98
- case 'text':
99
- s.appendAssistantText(assistantId, chunk.content ?? '');
100
- break;
101
- case 'tool_call':
102
- s.startStep(chunk.tool_name, assistantId);
103
- break;
104
- case 'tool_result': {
105
- const tr = chunk.tool_result;
106
- const success = tr?.success !== false && !tr?.error;
107
- s.resolveLastStep(chunk.tool_name, success ? 'success' : 'error', tr?.data, tr?.error);
108
- // Stash the agent config in state (used as `agentConfig` context on
109
- // the next request) and remember its id. We no longer render it.
110
- const data = tr?.data as Record<string, unknown> | undefined;
111
- if (data) {
112
- const cfg = data.config ?? data.agentConfig ?? data.agent ?? null;
113
- if (cfg && typeof cfg === 'object') {
114
- s.setAgentConfig(cfg);
115
- const idCandidate = (cfg as Record<string, unknown>).id;
116
- if (typeof idCandidate === 'string') s.setAgentId(idCandidate);
117
- }
118
- }
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;
125
- // No link to show (e.g. alreadyConnected without a new url) → skip card.
126
- if (linkData.shortUrl) {
127
- // Cancel any previous active polling before starting a new one.
128
- stopConnectPolling();
129
- s.startConnectCard(channel, linkData, assistantId);
130
- connectPollingRef.current = startConnectPolling(
131
- workspaceOptsFromStream(stream),
132
- channel,
133
- linkData,
134
- {
135
- onAttempt(attempt) {
136
- useStudioStore.getState().updateConnectCard({ attempt });
137
- },
138
- onActivating() {
139
- useStudioStore.getState().setConnectStatus('activating');
140
- },
141
- onConnected(label) {
142
- useStudioStore.getState().setConnectStatus('connected', { connectedLabel: label });
143
- connectPollingRef.current = null;
144
- if (!streamingRef.current) {
145
- const channelName =
146
- channel === 'whatsapp' ? 'WhatsApp'
147
- : channel === 'instagram' ? 'Instagram'
148
- : 'Messenger';
149
- const followUp =
150
- `My ${channelName} is now connected. ${label ? `Label: ${label}.` : ''} ` +
151
- `Continue with the agent setup.`;
152
- void sendBackendRef.current(followUp);
153
- }
154
- },
155
- onTimeout() {
156
- useStudioStore.getState().setConnectStatus('timeout');
157
- connectPollingRef.current = null;
158
- },
159
- onError(message) {
160
- useStudioStore.getState().setConnectStatus('error', { errorMessage: message });
161
- connectPollingRef.current = null;
162
- },
163
- },
164
- );
165
- }
166
- }
167
- break;
168
- }
169
- case 'usage':
170
- s.addUsage(chunk.input_tokens ?? 0, chunk.output_tokens ?? 0);
171
- break;
172
- case 'error':
173
- s.failAllRunningSteps(chunk.error);
174
- s.pushSyntheticAssistant(`✖ ${chunk.error}`, { error: true });
175
- break;
176
- case 'done':
177
- break;
178
- }
179
- };
180
-
181
- try {
182
- await streamStudio(
183
- stream,
184
- {
185
- message: userText,
186
- messages: historyForApi,
187
- agentId: useStudioStore.getState().agentId,
188
- agentConfig: useStudioStore.getState().agentConfig,
189
- },
190
- onChunk,
191
- controller.signal,
192
- );
193
- } catch (err) {
194
- const msg = formatError(err, dev);
195
- useStudioStore.getState().failAllRunningSteps('Network error');
196
- useStudioStore.getState().pushSyntheticAssistant(msg, { error: true });
197
- } finally {
198
- useStudioStore.getState().finishAssistantMessage(assistantId);
199
- useStudioStore.getState().setStreaming(false);
200
- useStudioStore.getState().setAbortController(null);
201
- // /compact flow: replace history with the assistant summary just produced.
202
- if (useStudioStore.getState().pendingCompact) {
203
- useStudioStore.getState().compactInto(assistantId);
204
- }
205
- }
206
- }, [stream, dev, stopConnectPolling]);
207
-
208
- useEffect(() => { sendBackendRef.current = sendToBackend; }, [sendToBackend]);
209
-
210
- const handleSubmit = useCallback(async (raw: string) => {
211
- const parsed = parseSlash(raw);
212
- if (parsed) {
213
- const spec = slashRegistry.get(parsed.name);
214
- if (!spec) {
215
- useStudioStore.getState().pushSyntheticAssistant(
216
- `Unknown command: \`/${parsed.name}\`. Run \`/help\` to see the list.`,
217
- { error: true },
218
- );
219
- return;
220
- }
221
- const res = await spec.handler(parsed, { exit });
222
- if (res.kind === 'message') {
223
- await sendToBackend(res.content);
224
- }
225
- return;
226
- }
227
- await sendToBackend(raw);
228
- }, [sendToBackend, exit]);
229
-
230
- const empty = messages.length === 0;
231
-
232
- return (
233
- <Box flexDirection="column">
234
- <Header
235
- version={version}
236
- workspace={stream.workspaceId}
237
- zone={stream.zone}
238
- agentId={agentId}
239
- dev={dev}
240
- supportMode={!!supportMode}
241
- />
242
-
243
- <Box flexDirection="column" paddingX={1}>
244
- {empty ? <Suggestions /> : <ChatLog />}
245
- </Box>
246
-
247
- <Input disabled={streaming} onSubmit={handleSubmit} />
248
- <Footer
249
- inputTokens={usage.inputTokens}
250
- outputTokens={usage.outputTokens}
251
- streaming={streaming}
252
- />
253
- </Box>
254
- );
255
- }
256
-
257
- function formatError(err: unknown, dev: boolean): string {
258
- if (err instanceof StudioHttpError) {
259
- if (err.status === 401) return '✖ Token expired or invalid. Run `plazbot init` with a valid JWT.';
260
- if (err.status === 403) return '✖ No permission for this workspace.';
261
- if (err.status === 429) return '✖ Rate limit reached. Wait a few seconds and try again.';
262
- if (err.status >= 500) {
263
- return dev
264
- ? `✖ Backend error ${err.status}.\n\`\`\`\n${err.body ?? err.statusText}\n\`\`\``
265
- : `✖ The backend returned error ${err.status}. Retry in a few seconds.`;
266
- }
267
- return `✖ HTTP ${err.status} ${err.statusText}`;
268
- }
269
- if (err instanceof Error) {
270
- if (err.name === 'AbortError') return '✖ Stream cancelled.';
271
- return `✖ ${err.message}`;
272
- }
273
- return '✖ Unknown error';
274
- }
@@ -1,121 +0,0 @@
1
- import React from 'react';
2
- import { Box, Static } from 'ink';
3
- import { useStudioStore } from '../state/store.js';
4
- import type { ChatMessage, ToolStep } from '../state/store.js';
5
- import type { ConnectState } from '../connect/types.js';
6
- import { Message } from './Message.js';
7
- import { ToolCall } from './ToolCall.js';
8
- import { ConnectCard } from './ConnectCard.js';
9
-
10
- type HistoricItem =
11
- | { kind: 'message'; key: string; message: ChatMessage }
12
- | { kind: 'step'; key: string; step: ToolStep }
13
- | { kind: 'connectCard'; key: string; state: ConnectState };
14
-
15
- /**
16
- * Renderiza el log de la conversacion.
17
- *
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.
21
- *
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
- */
26
- export function ChatLog(): React.ReactElement {
27
- const messages = useStudioStore((s) => s.messages);
28
- const steps = useStudioStore((s) => s.steps);
29
- const connectCard = useStudioStore((s) => s.connectCard);
30
-
31
- const stepsByMsg = new Map<string, ToolStep[]>();
32
- for (const step of steps) {
33
- const arr = stepsByMsg.get(step.afterMessageId) ?? [];
34
- arr.push(step);
35
- stepsByMsg.set(step.afterMessageId, arr);
36
- }
37
-
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
50
- const dynamicMsgIds = new Set<string>();
51
- const last = messages[messages.length - 1];
52
- if (last?.streaming) dynamicMsgIds.add(last.id);
53
- for (const step of steps) {
54
- if (step.status === 'running') dynamicMsgIds.add(step.afterMessageId);
55
- }
56
- if (cardActive && connectCard) dynamicMsgIds.add(connectCard.anchorMessageId);
57
-
58
- const staticItems: HistoricItem[] = [];
59
- let cardInjected = false;
60
- for (const m of messages) {
61
- if (dynamicMsgIds.has(m.id)) continue;
62
- staticItems.push({ kind: 'message', key: `m-${m.id}`, message: m });
63
- for (const s of stepsByMsg.get(m.id) ?? []) {
64
- staticItems.push({ kind: 'step', key: `s-${s.id}`, step: s });
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
- });
84
- }
85
-
86
- const dynamicMessages = messages.filter((m) => dynamicMsgIds.has(m.id));
87
-
88
- return (
89
- <Box flexDirection="column">
90
- <Static items={staticItems}>
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
- }}
100
- </Static>
101
-
102
- <Box flexDirection="column">
103
- {dynamicMessages.map((m) => (
104
- <React.Fragment key={m.id}>
105
- <Message message={m} />
106
- {(stepsByMsg.get(m.id) ?? []).map((s) => (
107
- <ToolCall key={s.id} step={s} />
108
- ))}
109
- {cardActive && connectCard && connectCard.anchorMessageId === m.id ? (
110
- <ConnectCard state={connectCard} />
111
- ) : null}
112
- </React.Fragment>
113
- ))}
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} />
117
- ) : null}
118
- </Box>
119
- </Box>
120
- );
121
- }
@@ -1,171 +0,0 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import Spinner from 'ink-spinner';
4
- import type { ConnectChannel, ConnectState } from '../connect/types.js';
5
- import { CONNECT_POLL_MAX_ATTEMPTS } from '../connect/types.js';
6
-
7
- interface Props {
8
- state: ConnectState;
9
- }
10
-
11
- function formatExpiry(iso?: string): string {
12
- if (!iso) return '';
13
- const d = new Date(iso);
14
- if (Number.isNaN(d.getTime())) return '';
15
- return d.toLocaleString('en-US', {
16
- day: 'numeric',
17
- month: 'short',
18
- hour: '2-digit',
19
- minute: '2-digit',
20
- });
21
- }
22
-
23
- /**
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
- */
28
- function osc8(url: string, label: string): string {
29
- return `\u001b]8;;${url}\u001b\\${label}\u001b]8;;\u001b\\`;
30
- }
31
-
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;
71
- const shortUrl = linkData.shortUrl || '';
72
- const expiry = formatExpiry(linkData.expiresAt);
73
- const copy = copyFor(channel);
74
- const typeLabel = copy.typeLabel(linkData.linkType);
75
-
76
- let headerIcon = '●';
77
- let headerColor: 'green' | 'red' | 'yellow' = 'green';
78
- let headerText = copy.headerWaiting;
79
- if (status === 'connected') {
80
- headerIcon = '✓';
81
- headerColor = 'green';
82
- headerText = copy.headerConnected;
83
- } else if (status === 'error') {
84
- headerIcon = '✖';
85
- headerColor = 'red';
86
- headerText = 'Could not activate the integration';
87
- } else if (status === 'timeout') {
88
- headerIcon = '○';
89
- headerColor = 'yellow';
90
- headerText = 'No connection detected';
91
- } else if (status === 'cancelled') {
92
- headerIcon = '○';
93
- headerColor = 'yellow';
94
- headerText = 'Connection cancelled';
95
- }
96
-
97
- const showLink = status !== 'connected' && status !== 'cancelled' && !!shortUrl;
98
-
99
- return (
100
- <Box
101
- flexDirection="column"
102
- borderStyle="round"
103
- borderColor={status === 'error' ? 'red' : status === 'connected' ? 'green' : 'gray'}
104
- paddingX={1}
105
- marginY={1}
106
- marginLeft={2}
107
- >
108
- {/* Header */}
109
- <Box>
110
- <Text bold color={headerColor}>{headerIcon} </Text>
111
- <Text bold>{headerText}</Text>
112
- {status !== 'connected' && status !== 'error' && status !== 'cancelled' ? (
113
- <>
114
- <Text dimColor> </Text>
115
- <Text dimColor>· {typeLabel}</Text>
116
- </>
117
- ) : null}
118
- </Box>
119
-
120
- {/* Link block */}
121
- {showLink ? (
122
- <Box marginTop={1} flexDirection="column">
123
- <Box>
124
- <Text dimColor>link: </Text>
125
- <Text color="cyan" underline>{osc8(shortUrl, shortUrl)}</Text>
126
- </Box>
127
- {expiry ? (
128
- <Box>
129
- <Text dimColor>expires: {expiry}</Text>
130
- </Box>
131
- ) : null}
132
- <Box marginTop={1}>
133
- <Text dimColor>{copy.hint}</Text>
134
- </Box>
135
- </Box>
136
- ) : null}
137
-
138
- {/* Status line */}
139
- <Box marginTop={1}>
140
- {status === 'waiting' ? (
141
- <>
142
- <Text color="yellow"><Spinner type="dots" /></Text>
143
- <Text dimColor> Waiting for connection… </Text>
144
- <Text dimColor>({attempt}/{CONNECT_POLL_MAX_ATTEMPTS})</Text>
145
- <Text dimColor> · press Esc to cancel</Text>
146
- </>
147
- ) : null}
148
- {status === 'activating' ? (
149
- <>
150
- <Text color="yellow"><Spinner type="dots" /></Text>
151
- <Text dimColor> Connection detected, activating…</Text>
152
- </>
153
- ) : null}
154
- {status === 'connected' ? (
155
- <Text color="green">{copy.connectedTpl(connectedLabel || '')}</Text>
156
- ) : null}
157
- {status === 'timeout' ? (
158
- <Text dimColor>
159
- We didn't detect the connection within 5 minutes. Ask the agent to try again.
160
- </Text>
161
- ) : null}
162
- {status === 'cancelled' ? (
163
- <Text dimColor>Polling cancelled by user.</Text>
164
- ) : null}
165
- {status === 'error' ? (
166
- <Text color="red">{errorMessage || 'An error occurred while activating the integration.'}</Text>
167
- ) : null}
168
- </Box>
169
- </Box>
170
- );
171
- }