ghostykit 0.5.2 → 0.6.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.
package/README.md CHANGED
@@ -111,10 +111,17 @@ ref.current?.send("Why did this fail?"); // ask on the user'
111
111
  Other options: `strings`, `toolIcons`, `formatUserText`, `markdown`, `features`
112
112
  (`attachments`, `retry`, `copy`, `usage`, `replyOptions`, `duration`, `steer`), `references` /
113
113
  `onReferencesChange`, `voiceNoteText`, `placeholder`, `avatar`, `initialTurns`, `loadOlder` (earlier
114
- pages of a long thread), the slot `sidePanel`, and the callbacks `onTurnStart`, `onTitle`, `onState`,
115
- `onEvent` (every raw AG-UI event), `onSend`. The agent's shared state (`STATE_SNAPSHOT` +
116
- `STATE_DELTA`) is `pilot.state.agentState`, and each turn's `CUSTOM` events are `turn.custom` (what
117
- `renderDeliveries` draws). To read the state outside the chat, call `usePilot(transport)` yourself
114
+ pages of a long thread), `forwardedProps` (extra keys for every run, e.g. `{permissions: "ask"}`), the
115
+ slots `sidePanel` and `aboveComposer` (both may take the `pilot`), and the callbacks `onTurnStart`,
116
+ `onTitle`, `onState`, `onEvent` (every raw AG-UI event), `onSend`. The agent's shared state
117
+ (`STATE_SNAPSHOT` + `STATE_DELTA`) is `pilot.state.agentState`, and each turn's `CUSTOM` events are
118
+ `turn.custom` (what `renderDeliveries` draws).
119
+
120
+ Subagents (`state.subagents.tasks`) are `pilot.subagents`; `pilot.stopSubagent(id)` stops one, and
121
+ `<Subagents pilot={pilot} />` is a ready-made bar ("2 agents working", one row each, *Stop*):
122
+ `slots={{ aboveComposer: (pilot) => <Subagents pilot={pilot} /> }}`. A message the server can't take
123
+ while a run is live (`409 run_active`) waits in `pilot.state.queued` and is sent when that run ends.
124
+ `pilot.refresh()` asks for the thread again (when your own event bus says something changed). To read the state outside the chat, call `usePilot(transport)` yourself
118
125
  and pass the result as `pilot`. Every piece is
119
126
  exported (`AgentBubble`, `AgentSteps`, `Composer`, `ConfirmCard`, `Markdown`, …) if you want to build
120
127
  your own layout.
@@ -111,6 +111,8 @@ function createPilotState(turns = []) {
111
111
  step: null,
112
112
  history: null,
113
113
  olderCount: 0,
114
+ queued: [],
115
+ openText: null,
114
116
  effects: []
115
117
  };
116
118
  }
@@ -255,7 +257,9 @@ function applySnapshot(s, messages) {
255
257
  turns: [...prefix, ...turns],
256
258
  olderCount: prefix.length,
257
259
  history: keepOlder ? s.history : s.historyNext ?? null,
258
- historyNext: void 0
260
+ historyNext: void 0,
261
+ // Lo abierto no viene en el snapshot: si sigue, el servidor lo re-emite enseguida.
262
+ openText: null
259
263
  };
260
264
  }
261
265
  function reduceAgUiEvent(prev, e, now = Date.now()) {
@@ -263,20 +267,30 @@ function reduceAgUiEvent(prev, e, now = Date.now()) {
263
267
  switch (e.type) {
264
268
  case "RUN_STARTED":
265
269
  return { ...s, busy: true, error: null, step: null, runId: e.runId ?? s.runId };
266
- case "TEXT_MESSAGE_START":
270
+ case "TEXT_MESSAGE_START": {
267
271
  if (e.role && e.role !== "assistant") return s;
268
- return patchAssistant(s, (t) => ({ ...t, text: t.text && !t.text.endsWith("\n") ? `${t.text}
272
+ const id = typeof e.messageId === "string" ? e.messageId : null;
273
+ const last = s.turns[s.turns.length - 1];
274
+ if (id && s.openText?.id === id && last?.role === "assistant" && last.text.length >= s.openText.start) {
275
+ const start = s.openText.start;
276
+ return patchAssistant(s, (t) => ({ ...t, text: t.text.slice(0, start) }));
277
+ }
278
+ const next = patchAssistant(s, (t) => ({ ...t, text: t.text && !t.text.endsWith("\n") ? `${t.text}
269
279
 
270
280
  ` : t.text }));
281
+ return { ...next, openText: id ? { id, start: next.turns[next.turns.length - 1].text.length } : null };
282
+ }
271
283
  case "TEXT_MESSAGE_CONTENT":
272
284
  case "TEXT_MESSAGE_CHUNK":
273
285
  if (e.role && e.role !== "assistant") return s;
274
286
  if (typeof e.delta !== "string" || !e.delta) return s;
275
287
  return patchAssistant(s, (t) => ({ ...t, text: t.text + e.delta }));
276
288
  case "TEXT_MESSAGE_END":
277
- return s;
278
- case "TOOL_CALL_START":
279
- return upsertTool(s, e.toolCallId, { title: e.toolCallName, status: "in_progress" });
289
+ return s.openText && s.openText.id === e.messageId ? { ...s, openText: null } : s;
290
+ case "TOOL_CALL_START": {
291
+ const known = s.turns[s.turns.length - 1]?.tools?.some((x) => x.id === e.toolCallId);
292
+ return upsertTool(s, e.toolCallId, { title: e.toolCallName, status: "in_progress", ...known ? { args: "" } : {} });
293
+ }
280
294
  case "TOOL_CALL_CHUNK": {
281
295
  const known = s.turns[s.turns.length - 1]?.tools?.find((x) => x.id === e.toolCallId);
282
296
  let next = known ? s : upsertTool(s, e.toolCallId, { title: e.toolCallName, status: "in_progress" });
@@ -355,7 +369,7 @@ function toHistory(v) {
355
369
  return { hasMoreBefore: h.hasMoreBefore === true, ...typeof h.before === "string" ? { before: h.before } : {} };
356
370
  }
357
371
  function finish(s0, e, now, failed = false) {
358
- let s = { ...s0, busy: false, step: null };
372
+ let s = { ...s0, busy: false, step: null, openText: null };
359
373
  const outcome = e.outcome;
360
374
  const interrupted = outcome?.type === "interrupt" && Array.isArray(outcome.interrupts) && outcome.interrupts.length > 0;
361
375
  const clockOffset = typeof e.timestamp === "number" ? now - e.timestamp : 0;
@@ -395,7 +409,32 @@ function beginSend(prev, text, attachments = [], now = Date.now()) {
395
409
  return { ...prev, turns, busy: true, error: null, notice: null, turnStartedAt: now, effects: [{ type: "turn-start" }] };
396
410
  }
397
411
  function beginSteer(prev, text) {
398
- return { ...prev, turns: [...prev.turns, { role: "user", text }], notice: null, effects: [] };
412
+ return { ...prev, turns: [...prev.turns, { role: "user", text }], notice: null, openText: null, effects: [] };
413
+ }
414
+ function queueMessage(prev, text, attachments = [], tries = 1) {
415
+ const turns = [...prev.turns];
416
+ for (let i = turns.length - 1; i >= 0; i--) {
417
+ if (turns[i].role === "user" && turns[i].text === text) {
418
+ turns.splice(i, 1);
419
+ break;
420
+ }
421
+ }
422
+ return { ...prev, turns, queued: [...prev.queued, { text, ...attachments.length ? { attachments } : {}, tries }], effects: [] };
423
+ }
424
+ function takeQueued(prev) {
425
+ return { state: { ...prev, queued: [], effects: [] }, queued: prev.queued };
426
+ }
427
+ function subagentsOf(agentState) {
428
+ const tasks = agentState.subagents?.tasks;
429
+ if (!Array.isArray(tasks)) return [];
430
+ return tasks.filter((t) => Boolean(t) && typeof t === "object" && typeof t.id === "string");
431
+ }
432
+ function markSubagent(prev, taskId, status) {
433
+ const tasks = subagentsOf(prev.agentState);
434
+ if (!tasks.some((t) => t.id === taskId)) return prev;
435
+ const subagents = { ...prev.agentState.subagents, tasks: tasks.map((t) => t.id === taskId ? { ...t, status } : t) };
436
+ const agentState = { ...prev.agentState, subagents };
437
+ return { ...prev, agentState, effects: [{ type: "state", state: agentState }] };
399
438
  }
400
439
  function prependHistory(prev, messages, page) {
401
440
  const have = new Set(prev.turns.map((t) => t.id).filter(Boolean));
@@ -406,7 +445,10 @@ function sendFailed(prev, message) {
406
445
  return { ...prev, busy: false, turnStartedAt: 0, error: message, effects: [] };
407
446
  }
408
447
  function stopTurn(prev) {
409
- return { ...prev, busy: false, step: null, turnStartedAt: 0, turns: closeOpenTools(prev.turns, "cancelled"), effects: [] };
448
+ const text = prev.queued.map((q) => q.text).filter(Boolean).join("\n\n");
449
+ const attachments = prev.queued.flatMap((q) => q.attachments ?? []);
450
+ const returnedDraft = prev.queued.length ? { text, ...attachments.length ? { attachments } : {}, n: (prev.returnedDraft?.n ?? 0) + 1 } : prev.returnedDraft;
451
+ return { ...prev, busy: false, step: null, queued: [], returnedDraft, turnStartedAt: 0, turns: closeOpenTools(prev.turns, "cancelled"), effects: [] };
410
452
  }
411
453
  function dropLastExchange(prev) {
412
454
  const turns = [...prev.turns];
@@ -648,6 +690,10 @@ export {
648
690
  reduceAgUiEvent,
649
691
  beginSend,
650
692
  beginSteer,
693
+ queueMessage,
694
+ takeQueued,
695
+ subagentsOf,
696
+ markSubagent,
651
697
  prependHistory,
652
698
  sendFailed,
653
699
  stopTurn,
@@ -1,5 +1,5 @@
1
1
  export * from "./types";
2
- export { argsDetail, beginSend, beginSteer, createPilotState, dropLastExchange, expiryMs, lastUserTurn, messagesToTurns, prependHistory, reduceAgUiEvent, resolvePermission, sendFailed, stopTurn, toPermission, } from "./reducer";
2
+ export { argsDetail, beginSend, beginSteer, createPilotState, dropLastExchange, expiryMs, lastUserTurn, markSubagent, messagesToTurns, prependHistory, queueMessage, reduceAgUiEvent, resolvePermission, sendFailed, stopTurn, subagentsOf, takeQueued, toPermission, } from "./reducer";
3
3
  export { applyJsonPatch, type PatchOp } from "./jsonPatch";
4
4
  export { isNarration, narrationToMarkdown, splitNarration } from "./narration";
5
5
  export { cleanReplyOption, extractReplyOptions, type ReplyOptions } from "./options";
@@ -14,20 +14,24 @@ import {
14
14
  isNarration,
15
15
  isRunning,
16
16
  lastUserTurn,
17
+ markSubagent,
17
18
  messagesToTurns,
18
19
  narrationToMarkdown,
19
20
  prependHistory,
21
+ queueMessage,
20
22
  reduceAgUiEvent,
21
23
  resolvePermission,
22
24
  sendFailed,
23
25
  splitNarration,
24
26
  stepLabel,
25
27
  stopTurn,
28
+ subagentsOf,
26
29
  summarizeTools,
30
+ takeQueued,
27
31
  toPermission,
28
32
  toolName,
29
33
  toolNames
30
- } from "../chunks/chunk-3XDAYARB.js";
34
+ } from "../chunks/chunk-4XKQ6XAJ.js";
31
35
  import {
32
36
  applyReferences,
33
37
  cleanUserText,
@@ -53,17 +57,21 @@ export {
53
57
  isNarration,
54
58
  isRunning,
55
59
  lastUserTurn,
60
+ markSubagent,
56
61
  messagesToTurns,
57
62
  narrationToMarkdown,
58
63
  prependHistory,
64
+ queueMessage,
59
65
  reduceAgUiEvent,
60
66
  resolvePermission,
61
67
  sendFailed,
62
68
  splitNarration,
63
69
  stepLabel,
64
70
  stopTurn,
71
+ subagentsOf,
65
72
  summarizeTools,
66
73
  systemNoteOf,
74
+ takeQueued,
67
75
  toPermission,
68
76
  toolName,
69
77
  toolNames
@@ -1,5 +1,5 @@
1
1
  import type { Interrupt, Message } from "@ag-ui/core";
2
- import type { Attachment, Decision, HistoryPage, PermissionRequest, PilotState, Turn } from "./types";
2
+ import type { Attachment, Decision, HistoryPage, PermissionRequest, PilotState, SubagentTask, Turn } from "./types";
3
3
  export declare function createPilotState(turns?: Turn[]): PilotState;
4
4
  type Ev = {
5
5
  type: string;
@@ -23,6 +23,20 @@ export declare function beginSend(prev: PilotState, text: string, attachments?:
23
23
  * diga después va en una respuesta nueva debajo; el run (y su reloj) siguen siendo los mismos.
24
24
  */
25
25
  export declare function beginSteer(prev: PilotState, text: string): PilotState;
26
+ /**
27
+ * El servidor no dejó entrar el mensaje (409 `run_active` / `interrupt_pending`): su burbuja sale del
28
+ * hilo y queda en espera; se manda solo cuando termine el run vivo (gs-agui 1 §0.7b-6).
29
+ */
30
+ export declare function queueMessage(prev: PilotState, text: string, attachments?: Attachment[], tries?: number): PilotState;
31
+ /** Saca los mensajes en espera (para mandarlos juntos en un run). */
32
+ export declare function takeQueued(prev: PilotState): {
33
+ state: PilotState;
34
+ queued: PilotState["queued"];
35
+ };
36
+ /** Los subagentes del estado del agente (`state.subagents.tasks`); lo que no tiene `id` se descarta. */
37
+ export declare function subagentsOf(agentState: Record<string, unknown>): SubagentTask[];
38
+ /** Marca un subagente (p. ej. `stopped` al pedir detenerlo); el siguiente STATE_* manda. */
39
+ export declare function markSubagent(prev: PilotState, taskId: string, status: SubagentTask["status"]): PilotState;
26
40
  /**
27
41
  * Mensajes más viejos (`loadOlder`) delante del hilo. Las páginas empiezan en un mensaje del usuario;
28
42
  * lo que ya se tenía (mismo id) no se repite.
@@ -46,6 +46,34 @@ export interface CustomEntry {
46
46
  value: unknown;
47
47
  timestamp?: number;
48
48
  }
49
+ /** Un subagente del agente (`state.subagents.tasks[]`, gs-agui 1 §0.5). */
50
+ export interface SubagentTask {
51
+ id: string;
52
+ title?: string;
53
+ status: "running" | "completed" | "failed" | "stopped" | (string & {});
54
+ /** Epoch ms. */
55
+ startedAt?: number;
56
+ endedAt?: number;
57
+ usage?: {
58
+ tokens?: number;
59
+ toolUses?: number;
60
+ };
61
+ /** La última herramienta que usó (para el renglón «Leyendo…»). */
62
+ lastTool?: string;
63
+ /** Lo que dijo al terminar. */
64
+ summary?: string;
65
+ model?: string;
66
+ effort?: string;
67
+ /** Su resultado ya entró al hilo. */
68
+ delivered?: boolean;
69
+ }
70
+ /** Un mensaje que no pudo entrar al run vivo (409 `run_active`): sale solo al terminar el run. */
71
+ export interface QueuedMessage {
72
+ text: string;
73
+ attachments?: Attachment[];
74
+ /** Cuántas veces el servidor lo rechazó (se rinde a la tercera). */
75
+ tries: number;
76
+ }
49
77
  /** Hay mensajes más viejos que los del snapshot (`CUSTOM ghosty.history`). */
50
78
  export interface HistoryPage {
51
79
  hasMoreBefore: boolean;
@@ -135,6 +163,20 @@ export interface PilotState {
135
163
  olderCount: number;
136
164
  /** El `ghosty.history` que acompaña al snapshot que viene. */
137
165
  historyNext?: HistoryPage | null;
166
+ /** Mensajes en espera de que termine el run vivo (steer rechazado). */
167
+ queued: QueuedMessage[];
168
+ /** Lo que estaba en espera cuando la persona detuvo: regresa a la caja (`n` cambia en cada vez). */
169
+ returnedDraft?: {
170
+ text: string;
171
+ attachments?: Attachment[];
172
+ n: number;
173
+ } | null;
174
+ /** El `TEXT_MESSAGE` abierto y dónde empieza en el texto del último turno: si el servidor lo
175
+ * re-emite (reconexión, §0.7b-4) con el mismo `messageId`, se reescribe en vez de duplicarse. */
176
+ openText?: {
177
+ id: string;
178
+ start: number;
179
+ } | null;
138
180
  /** Efectos de la ÚLTIMA transición. El hook los ejecuta y los descarta. */
139
181
  effects: PilotEffect[];
140
182
  }
@@ -31,10 +31,10 @@ export interface PilotoSlots {
31
31
  emptyState?: ReactNode;
32
32
  /** Arriba a la derecha, sobre el hilo (compartir, abrir en grande…). */
33
33
  headerActions?: ReactNode;
34
- /** Pegado arriba del composer (un aviso, un banner). */
35
- aboveComposer?: ReactNode;
36
- /** Un panel junto al chat (subagentes, vista previa), con el estado del agente. */
37
- sidePanel?: (agentState: Record<string, unknown>) => ReactNode;
34
+ /** Pegado arriba del composer (un aviso, un banner, `<Subagents pilot={pilot} />`). */
35
+ aboveComposer?: ReactNode | ((pilot: Pilot) => ReactNode);
36
+ /** Un panel junto al chat (subagentes, vista previa), con el estado del agente y el piloto. */
37
+ sidePanel?: (agentState: Record<string, unknown>, pilot: Pilot) => ReactNode;
38
38
  }
39
39
  export interface PilotoProps extends UsePilotOptions {
40
40
  transport: PilotTransport | null;
@@ -0,0 +1,10 @@
1
+ import type { Pilot } from "./usePilot";
2
+ export interface SubagentsProps {
3
+ pilot: Pick<Pilot, "subagents" | "stopSubagent">;
4
+ /** Abierta de entrada. Default: no. */
5
+ defaultOpen?: boolean;
6
+ /** Los que ya entregaron también se listan. Default: sí. */
7
+ showFinished?: boolean;
8
+ className?: string;
9
+ }
10
+ export declare function Subagents({ pilot, defaultOpen, showFinished, className }: SubagentsProps): import("react").JSX.Element | null;
@@ -9,6 +9,7 @@ export { Composer, ModelPicker, mimeOf, type ComposerProps, type ModelOption, ty
9
9
  export { ConfirmCard, DecisionLines } from "./ConfirmCard";
10
10
  export { ReferenceChips, Suggestions } from "./Chips";
11
11
  export { TypingDots, WaitLabel } from "./Status";
12
+ export { Subagents, type SubagentsProps } from "./Subagents";
12
13
  export { PilotoContext, usePiloto, type MarkdownOptions, type PilotIcon, type PilotoConfig, type PilotTheme } from "./context";
13
14
  export { STRINGS_EN, STRINGS_ES, type PilotStrings } from "./strings";
14
15
  export * from "../core";
@@ -14,20 +14,24 @@ import {
14
14
  isNarration,
15
15
  isRunning,
16
16
  lastUserTurn,
17
+ markSubagent,
17
18
  messagesToTurns,
18
19
  narrationToMarkdown,
19
20
  prependHistory,
21
+ queueMessage,
20
22
  reduceAgUiEvent,
21
23
  resolvePermission,
22
24
  sendFailed,
23
25
  splitNarration,
24
26
  stepLabel,
25
27
  stopTurn,
28
+ subagentsOf,
26
29
  summarizeTools,
30
+ takeQueued,
27
31
  toPermission,
28
32
  toolName,
29
33
  toolNames
30
- } from "../chunks/chunk-3XDAYARB.js";
34
+ } from "../chunks/chunk-4XKQ6XAJ.js";
31
35
  import {
32
36
  PilotHttpError,
33
37
  createAgUiTransport,
@@ -821,7 +825,7 @@ import {
821
825
  useEffect as useEffect6,
822
826
  useImperativeHandle,
823
827
  useLayoutEffect,
824
- useMemo as useMemo3,
828
+ useMemo as useMemo4,
825
829
  useRef as useRef5,
826
830
  useState as useState9
827
831
  } from "react";
@@ -890,6 +894,11 @@ var STRINGS_ES = {
890
894
  steps: { waking: "Despertando al agente\u2026", session: "Preparando la sesi\xF3n\u2026", compacting: "Resumiendo la conversaci\xF3n\u2026" },
891
895
  loadOlder: "Cargar mensajes anteriores",
892
896
  loadingOlder: "Cargando\u2026",
897
+ queued: "Se manda cuando termine",
898
+ subagentsWorking: (n) => n === 1 ? "1 agente trabajando" : `${n} agentes trabajando`,
899
+ subagentsDone: (n) => n === 1 ? "1 agente termin\xF3" : `${n} agentes terminaron`,
900
+ subagentStop: "Detener",
901
+ subagentStatus: { running: "Trabajando", completed: "Termin\xF3", failed: "Fall\xF3", stopped: "Detenido" },
893
902
  confirmTitle: "Necesito tu permiso para",
894
903
  approve: "Aprobar",
895
904
  reject: "Rechazar",
@@ -958,6 +967,11 @@ var STRINGS_EN = {
958
967
  steps: { waking: "Waking the agent\u2026", session: "Starting the session\u2026", compacting: "Compacting the conversation\u2026" },
959
968
  loadOlder: "Load earlier messages",
960
969
  loadingOlder: "Loading\u2026",
970
+ queued: "Sends when this finishes",
971
+ subagentsWorking: (n) => n === 1 ? "1 agent working" : `${n} agents working`,
972
+ subagentsDone: (n) => n === 1 ? "1 agent finished" : `${n} agents finished`,
973
+ subagentStop: "Stop",
974
+ subagentStatus: { running: "Working", completed: "Done", failed: "Failed", stopped: "Stopped" },
961
975
  confirmTitle: "I need your permission to",
962
976
  approve: "Approve",
963
977
  reject: "Reject",
@@ -2295,7 +2309,8 @@ function WaitLabel({
2295
2309
  }
2296
2310
 
2297
2311
  // src/react/usePilot.ts
2298
- import { useCallback as useCallback2, useEffect as useEffect5, useRef as useRef4, useState as useState8 } from "react";
2312
+ import { useCallback as useCallback2, useEffect as useEffect5, useMemo as useMemo3, useRef as useRef4, useState as useState8 } from "react";
2313
+ var MAX_QUEUE_TRIES = 3;
2299
2314
  var uuid = () => {
2300
2315
  const c = globalThis.crypto;
2301
2316
  if (c?.randomUUID) return c.randomUUID();
@@ -2320,6 +2335,13 @@ function userMessage(text, attachments) {
2320
2335
  }
2321
2336
  return { id: uuid(), role: "user", content: parts };
2322
2337
  }
2338
+ function fromUserMessage(content) {
2339
+ if (typeof content === "string") return { text: content, attachments: [] };
2340
+ const parts = Array.isArray(content) ? content : [];
2341
+ const text = parts.filter((p) => p?.type === "text").map((p) => p.text ?? "").join("");
2342
+ const attachments = parts.filter((p) => p && p.type !== "text" && p.source).map((p) => ({ mimeType: p.mimeType ?? "application/octet-stream", ...p.filename ? { name: p.filename } : {}, ...p.source.type === "url" ? { url: p.source.value } : { data: p.source.value } }));
2343
+ return { text, attachments };
2344
+ }
2323
2345
  function usePilot(transport, opts = {}) {
2324
2346
  const [state, setState] = useState8(() => createPilotState(opts.initialTurns ?? []));
2325
2347
  const ref = useRef4(state);
@@ -2331,6 +2353,17 @@ function usePilot(transport, opts = {}) {
2331
2353
  const ctrl = useRef4(null);
2332
2354
  const liveRun = useRef4(null);
2333
2355
  const answers = useRef4(/* @__PURE__ */ new Map());
2356
+ const queuedTries = useRef4(/* @__PURE__ */ new Map());
2357
+ const flushRef = useRef4(() => {
2358
+ });
2359
+ const baseInput = () => {
2360
+ const ctx = optsRef.current.context;
2361
+ return { threadId, runId: uuid(), messages: [], tools: [], context: toAgUiContext(typeof ctx === "function" ? ctx() : ctx), state: {}, forwardedProps: extraProps() };
2362
+ };
2363
+ const extraProps = () => {
2364
+ const f = optsRef.current.forwardedProps;
2365
+ return (typeof f === "function" ? f() : f) ?? {};
2366
+ };
2334
2367
  const commit = useCallback2((next) => {
2335
2368
  if (next === ref.current) return;
2336
2369
  ref.current = next;
@@ -2347,20 +2380,12 @@ function usePilot(transport, opts = {}) {
2347
2380
  async (partial, reopening = false) => {
2348
2381
  const t = transportRef.current;
2349
2382
  if (!t) return;
2350
- const ctx = optsRef.current.context;
2351
- const input = {
2352
- threadId,
2353
- runId: uuid(),
2354
- tools: [],
2355
- context: toAgUiContext(typeof ctx === "function" ? ctx() : ctx),
2356
- state: {},
2357
- forwardedProps: {},
2358
- ...partial
2359
- };
2383
+ const input = { ...baseInput(), ...partial, forwardedProps: { ...extraProps(), ...partial.forwardedProps } };
2360
2384
  ctrl.current?.abort();
2361
2385
  const c = new AbortController();
2362
2386
  ctrl.current = c;
2363
2387
  const steering = typeof input.forwardedProps?.steer === "string";
2388
+ const userMsg = input.messages[0];
2364
2389
  if (!steering && (input.messages.length || input.resume?.length)) liveRun.current = input.runId;
2365
2390
  optsRef.current.onSend?.(input);
2366
2391
  let ended = false;
@@ -2382,7 +2407,18 @@ function usePilot(transport, opts = {}) {
2382
2407
  if (c.signal.aborted) return;
2383
2408
  const code = err instanceof PilotHttpError ? err.code : void 0;
2384
2409
  if ((code === "run_active" || code === "interrupt_pending") && !reopening) {
2385
- commit({ ...ref.current, notice: err instanceof Error ? err.message : null, effects: [] });
2410
+ if (userMsg) {
2411
+ const { text, attachments } = fromUserMessage(userMsg.content);
2412
+ const tries = (queuedTries.current.get(input.messages[0].id) ?? 0) + 1;
2413
+ if (tries < MAX_QUEUE_TRIES) {
2414
+ let s = queueMessage(ref.current, text, attachments, tries);
2415
+ if (!steering) s = { ...s, busy: false, turnStartedAt: 0 };
2416
+ commit({ ...s, notice: null });
2417
+ } else {
2418
+ commit(sendFailed(ref.current, err instanceof Error && err.message ? err.message : "No se pudo mandar el mensaje."));
2419
+ return;
2420
+ }
2421
+ } else commit({ ...ref.current, notice: err instanceof Error ? err.message : null, effects: [] });
2386
2422
  return runNow({ messages: [] }, true);
2387
2423
  }
2388
2424
  if (steering && !reopening) {
@@ -2392,7 +2428,11 @@ function usePilot(transport, opts = {}) {
2392
2428
  commit(sendFailed(ref.current, err instanceof Error && err.message ? err.message : "No se pudo hablar con el agente."));
2393
2429
  return;
2394
2430
  }
2395
- if (ended || c.signal.aborted) return;
2431
+ if (c.signal.aborted) return;
2432
+ if (ended) {
2433
+ if (ref.current.queued.length) setTimeout(() => flushRef.current(), 0);
2434
+ return;
2435
+ }
2396
2436
  if (!reopening) return runNow({ messages: [] }, true);
2397
2437
  commit(sendFailed(ref.current, "Se cort\xF3 la conexi\xF3n con el agente."));
2398
2438
  },
@@ -2418,16 +2458,38 @@ function usePilot(transport, opts = {}) {
2418
2458
  await runNow({ messages: [userMessage(text, [])], forwardedProps: { steer: live } });
2419
2459
  return;
2420
2460
  }
2461
+ await sendNew(text, attachments);
2462
+ },
2463
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2464
+ [commit, runNow]
2465
+ );
2466
+ const sendNew = useCallback2(
2467
+ async (text, attachments, tries = 0) => {
2421
2468
  const resume = pendingResume();
2422
2469
  answers.current.clear();
2423
2470
  let s = ref.current;
2424
2471
  for (const p of s.permissions) s = resolvePermission(s, p.id, "cancelled");
2425
2472
  commit(beginSend(s, text, attachments));
2426
- await runNow({ messages: [userMessage(text, attachments)], ...resume.length ? { resume } : {} });
2473
+ const msg = userMessage(text, attachments);
2474
+ if (tries) queuedTries.current.set(msg.id, tries);
2475
+ try {
2476
+ await runNow({ messages: [msg], ...resume.length ? { resume } : {} });
2477
+ } finally {
2478
+ queuedTries.current.delete(msg.id);
2479
+ }
2427
2480
  },
2428
2481
  // eslint-disable-next-line react-hooks/exhaustive-deps
2429
2482
  [commit, runNow]
2430
2483
  );
2484
+ flushRef.current = () => {
2485
+ const s = ref.current;
2486
+ if (!s.queued.length || s.busy || s.permissions.length || !transportRef.current) return;
2487
+ const { state: next, queued } = takeQueued(s);
2488
+ commit(next);
2489
+ const text = queued.map((q) => q.text).filter(Boolean).join("\n\n");
2490
+ const attachments = queued.flatMap((q) => q.attachments ?? []);
2491
+ void sendNew(text, attachments, Math.max(...queued.map((q) => q.tries)));
2492
+ };
2431
2493
  const stop = useCallback2(() => {
2432
2494
  const run = liveRun.current ?? (ref.current.busy ? ref.current.runId : null);
2433
2495
  liveRun.current = null;
@@ -2436,12 +2498,9 @@ function usePilot(transport, opts = {}) {
2436
2498
  commit(stopTurn(ref.current));
2437
2499
  const t = transportRef.current;
2438
2500
  if (!t || !run) return;
2439
- const ctx = optsRef.current.context;
2440
- void t.run(
2441
- { threadId, runId: uuid(), messages: [], tools: [], context: toAgUiContext(typeof ctx === "function" ? ctx() : ctx), state: {}, forwardedProps: { cancel: run } },
2442
- () => {
2443
- }
2444
- ).catch(() => {
2501
+ const input = baseInput();
2502
+ void t.run({ ...input, forwardedProps: { ...input.forwardedProps, cancel: run } }, () => {
2503
+ }).catch(() => {
2445
2504
  });
2446
2505
  }, [commit, threadId]);
2447
2506
  const retry = useCallback2(() => {
@@ -2498,8 +2557,24 @@ function usePilot(transport, opts = {}) {
2498
2557
  setLoadingOlder(false);
2499
2558
  }
2500
2559
  }, [commit]);
2560
+ const stopSubagent = useCallback2(
2561
+ async (taskId) => {
2562
+ const t = transportRef.current;
2563
+ if (!t) return;
2564
+ commit(markSubagent(ref.current, taskId, "stopped"));
2565
+ const input = baseInput();
2566
+ await t.run({ ...input, forwardedProps: { ...input.forwardedProps, stopSubagent: taskId } }, () => {
2567
+ }).catch(() => {
2568
+ });
2569
+ if (!ref.current.busy) await runNow({ messages: [] }, true);
2570
+ },
2571
+ // eslint-disable-next-line react-hooks/exhaustive-deps
2572
+ [commit, runNow]
2573
+ );
2574
+ const refresh = useCallback2(() => runNow({ messages: [] }, true), [runNow]);
2501
2575
  const canSteer = Boolean(transport) && (opts.steer ?? state.agentState.caps?.steer === true);
2502
- return { state, threadId, send, stop, retry, respondPermission, recordDecision, expirePermission, canSteer, loadOlder, loadingOlder };
2576
+ const subagents = useMemo3(() => subagentsOf(state.agentState), [state.agentState]);
2577
+ return { state, threadId, send, stop, retry, respondPermission, recordDecision, expirePermission, canSteer, loadOlder, loadingOlder, subagents, stopSubagent, refresh };
2503
2578
  }
2504
2579
 
2505
2580
  // src/react/Piloto.tsx
@@ -2512,8 +2587,8 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2512
2587
  const pilot = external ?? own;
2513
2588
  const { state } = pilot;
2514
2589
  const { turns, busy, error, notice, permissions, olderCount } = state;
2515
- const strings = useMemo3(() => ({ ...locale === "en" ? STRINGS_EN : STRINGS_ES, ...props.strings }), [locale, props.strings]);
2516
- const config = useMemo3(
2590
+ const strings = useMemo4(() => ({ ...locale === "en" ? STRINGS_EN : STRINGS_ES, ...props.strings }), [locale, props.strings]);
2591
+ const config = useMemo4(
2517
2592
  () => ({
2518
2593
  strings,
2519
2594
  toolVerbs: { ...locale === "en" ? TOOL_VERBS_EN : TOOL_VERBS_ES, ...props.toolVerbs },
@@ -2548,6 +2623,13 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2548
2623
  draftSeen.current = id;
2549
2624
  setDraftState((d) => ({ text: draftText, n: d.n + 1 }));
2550
2625
  }, [draftText, draftKey]);
2626
+ const returned = state.returnedDraft;
2627
+ useEffect6(() => {
2628
+ if (!returned) return;
2629
+ const typed = composerBox.current?.querySelector("textarea")?.value ?? "";
2630
+ const text = [returned.text, typed].filter((x) => x.trim()).join("\n\n");
2631
+ setDraftState((d) => ({ text, n: d.n + 1 }));
2632
+ }, [returned?.n]);
2551
2633
  const send = useCallback3(
2552
2634
  (text, attachments = []) => {
2553
2635
  const current = refsRef.current;
@@ -2709,8 +2791,9 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2709
2791
  ] });
2710
2792
  const withPanel = (chat) => slots.sidePanel ? /* @__PURE__ */ jsxs8("div", { className: "flex min-h-0 flex-1 flex-row", children: [
2711
2793
  /* @__PURE__ */ jsx10("div", { className: "relative flex min-h-0 min-w-0 flex-1 flex-col", children: chat }),
2712
- slots.sidePanel(state.agentState)
2794
+ slots.sidePanel(state.agentState, pilot)
2713
2795
  ] }) : chat;
2796
+ const aboveComposer = typeof slots.aboveComposer === "function" ? slots.aboveComposer(pilot) : slots.aboveComposer;
2714
2797
  const permissionCards = cards.map((p) => /* @__PURE__ */ jsx10(ConfirmCard, { request: p, onRespond: respond, onSettled: settle, onExpire: pilot.expirePermission }, p.id));
2715
2798
  return /* @__PURE__ */ jsx10(PilotoContext.Provider, { value: config, children: /* @__PURE__ */ jsx10("div", { ...rootProps(theme, props.skin), "data-busy": busy || void 0, className: `piloto ${props.className ?? ""}`, style: props.style, children: withPanel(empty ? (
2716
2799
  // Un hilo vacío enseña lo mismo que la portada: la cara, el saludo, la caja y por dónde
@@ -2729,7 +2812,7 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2729
2812
  )
2730
2813
  ] }),
2731
2814
  /* @__PURE__ */ jsxs8("div", { className: "w-full max-w-3xl", children: [
2732
- slots.aboveComposer,
2815
+ aboveComposer,
2733
2816
  composer,
2734
2817
  props.suggestions && /* @__PURE__ */ jsx10(Suggestions, { items: props.suggestions, onPick: (s) => send(s), onPrefill: (s) => setDraftState((d) => ({ text: s, n: d.n + 1 })) })
2735
2818
  ] })
@@ -2780,6 +2863,13 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2780
2863
  }
2781
2864
  ) }, i - olderCount);
2782
2865
  }),
2866
+ state.queued.map((q, i) => (
2867
+ // Esperan a que termine el run vivo (el servidor no los dejó entrar): salen solos.
2868
+ /* @__PURE__ */ jsxs8("div", { "data-queued": true, className: "flex flex-col items-end gap-1 opacity-60", children: [
2869
+ /* @__PURE__ */ jsx10(UserBubble, { turn: { role: "user", text: q.text, ...q.attachments ? { attachments: q.attachments } : {} } }),
2870
+ /* @__PURE__ */ jsx10("span", { className: "text-xs text-text-tertiary", children: t.queued })
2871
+ ] }, `q${i}`)
2872
+ )),
2783
2873
  permissionCards,
2784
2874
  busy && !toolRunning && !cards.length && /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-1.5", children: [
2785
2875
  /* @__PURE__ */ jsx10(TypingDots, {}),
@@ -2803,11 +2893,52 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2803
2893
  children: /* @__PURE__ */ jsx10(ArrowDown, { className: "h-4 w-4" })
2804
2894
  }
2805
2895
  ),
2806
- slots.aboveComposer,
2896
+ aboveComposer,
2807
2897
  composer
2808
2898
  ] })
2809
2899
  ] })) }) });
2810
2900
  });
2901
+
2902
+ // src/react/Subagents.tsx
2903
+ import { useState as useState10 } from "react";
2904
+ import { jsx as jsx11, jsxs as jsxs9 } from "react/jsx-runtime";
2905
+ var DOT = {
2906
+ running: "bg-brand-500 animate-pulse",
2907
+ completed: "bg-text-tertiary",
2908
+ failed: "bg-text-danger",
2909
+ stopped: "bg-text-disabled"
2910
+ };
2911
+ function Subagents({ pilot, defaultOpen = false, showFinished = true, className = "" }) {
2912
+ const { strings: t } = usePiloto();
2913
+ const [open, setOpen] = useState10(defaultOpen);
2914
+ const all = pilot.subagents;
2915
+ const tasks = showFinished ? all : all.filter((x) => x.status === "running");
2916
+ if (!tasks.length) return null;
2917
+ const running = all.filter((x) => x.status === "running").length;
2918
+ return /* @__PURE__ */ jsxs9("div", { "data-subagents": true, className: `mb-2 rounded-2xl border border-border-primary bg-background-secondary text-sm ${className}`, children: [
2919
+ /* @__PURE__ */ jsxs9("button", { type: "button", onClick: () => setOpen((o) => !o), "aria-expanded": open, className: "flex w-full items-center gap-2 px-3 py-2 text-left text-text-secondary", children: [
2920
+ /* @__PURE__ */ jsx11("span", { className: `h-2 w-2 shrink-0 rounded-full ${running ? DOT.running : DOT.completed}` }),
2921
+ /* @__PURE__ */ jsx11("span", { className: "flex-1 truncate", children: running ? t.subagentsWorking(running) : t.subagentsDone(tasks.length) }),
2922
+ /* @__PURE__ */ jsx11("span", { "aria-hidden": true, className: `text-text-tertiary transition-transform ${open ? "rotate-180" : ""}`, children: "\u2304" })
2923
+ ] }),
2924
+ open && /* @__PURE__ */ jsx11("ul", { className: "flex flex-col border-t border-border-primary", children: tasks.map((task) => /* @__PURE__ */ jsx11(SubagentRow, { task, onStop: () => void pilot.stopSubagent(task.id) }, task.id)) })
2925
+ ] });
2926
+ }
2927
+ function SubagentRow({ task, onStop }) {
2928
+ const { strings: t } = usePiloto();
2929
+ const line = task.status === "running" ? task.lastTool : task.summary ?? task.lastTool;
2930
+ return /* @__PURE__ */ jsxs9("li", { "data-subagent": task.id, "data-status": task.status, className: "flex items-center gap-2 px-3 py-2", children: [
2931
+ /* @__PURE__ */ jsx11("span", { className: `h-2 w-2 shrink-0 rounded-full ${DOT[task.status] ?? DOT.completed}` }),
2932
+ /* @__PURE__ */ jsxs9("div", { className: "min-w-0 flex-1", children: [
2933
+ /* @__PURE__ */ jsx11("div", { className: "truncate text-text-primary", children: task.title ?? task.id }),
2934
+ /* @__PURE__ */ jsxs9("div", { className: "truncate text-xs text-text-tertiary", children: [
2935
+ t.subagentStatus[task.status] ?? task.status,
2936
+ line ? ` \xB7 ${line}` : ""
2937
+ ] })
2938
+ ] }),
2939
+ task.status === "running" && /* @__PURE__ */ jsx11("button", { type: "button", onClick: onStop, className: "shrink-0 rounded-full border border-border-primary px-2.5 py-0.5 text-xs text-text-secondary transition hover:text-text-danger", children: t.subagentStop })
2940
+ ] });
2941
+ }
2811
2942
  export {
2812
2943
  AgentBubble,
2813
2944
  AgentSteps,
@@ -2831,6 +2962,7 @@ export {
2831
2962
  RobotArmIcon,
2832
2963
  STRINGS_EN,
2833
2964
  STRINGS_ES,
2965
+ Subagents,
2834
2966
  Suggestions,
2835
2967
  TOOL_VERBS_EN,
2836
2968
  TOOL_VERBS_ES,
@@ -2858,10 +2990,12 @@ export {
2858
2990
  isNarration,
2859
2991
  isRunning,
2860
2992
  lastUserTurn,
2993
+ markSubagent,
2861
2994
  messagesToTurns,
2862
2995
  mimeOf,
2863
2996
  narrationToMarkdown,
2864
2997
  prependHistory,
2998
+ queueMessage,
2865
2999
  readSse,
2866
3000
  reduceAgUiEvent,
2867
3001
  renderInline,
@@ -2870,8 +3004,10 @@ export {
2870
3004
  splitNarration,
2871
3005
  stepLabel,
2872
3006
  stopTurn,
3007
+ subagentsOf,
2873
3008
  summarizeTools,
2874
3009
  systemNoteOf,
3010
+ takeQueued,
2875
3011
  toAgUiContext,
2876
3012
  toPermission,
2877
3013
  toolName,
@@ -58,6 +58,14 @@ export interface PilotStrings {
58
58
  steps: Record<string, string>;
59
59
  loadOlder: string;
60
60
  loadingOlder: string;
61
+ /** Bajo un mensaje que espera a que termine el run vivo. */
62
+ queued: string;
63
+ /** «2 agentes trabajando». */
64
+ subagentsWorking: (n: number) => string;
65
+ /** Sin ninguno corriendo. */
66
+ subagentsDone: (n: number) => string;
67
+ subagentStop: string;
68
+ subagentStatus: Record<string, string>;
61
69
  confirmTitle: string;
62
70
  approve: string;
63
71
  reject: string;
@@ -1,5 +1,5 @@
1
1
  import type { BaseEvent, Message, RunAgentInput } from "@ag-ui/core";
2
- import type { Attachment, Decision, HistoryPage, PermissionRequest, PilotState, Reference, Turn } from "../core/types";
2
+ import type { Attachment, Decision, HistoryPage, PermissionRequest, PilotState, Reference, SubagentTask, Turn } from "../core/types";
3
3
  import { type PilotTransport } from "../transport";
4
4
  export interface TurnDoneInfo {
5
5
  /** El texto final de la respuesta. */
@@ -45,6 +45,11 @@ export interface UsePilotOptions {
45
45
  loadOlder?: (before: string | undefined) => Promise<{
46
46
  messages: Message[];
47
47
  } & HistoryPage>;
48
+ /**
49
+ * Lo que viaja en `forwardedProps` de cada run, además de lo del protocolo (`cancel`, `steer`,
50
+ * `stopSubagent`), p. ej. `{permissions: "ask"}` en gs. Una función se evalúa al mandar.
51
+ */
52
+ forwardedProps?: Record<string, unknown> | (() => Record<string, unknown>);
48
53
  }
49
54
  export interface SendOptions {
50
55
  references?: Reference[];
@@ -68,6 +73,15 @@ export interface Pilot {
68
73
  /** Antepone la página anterior del hilo (si hay `loadOlder` y `state.history.hasMoreBefore`). */
69
74
  loadOlder: () => Promise<void>;
70
75
  loadingOlder: boolean;
76
+ /** Los subagentes del agente (`state.subagents.tasks`), al día con cada STATE_SNAPSHOT/DELTA. */
77
+ subagents: SubagentTask[];
78
+ /** Detiene un subagente (`forwardedProps.stopSubagent`). No toca el run vivo. */
79
+ stopSubagent: (taskId: string) => Promise<void>;
80
+ /**
81
+ * Vuelve a pedir el hilo (POST sin mensaje → snapshot + estado + run vivo). Para cuando la app sabe
82
+ * por fuera que algo cambió (p. ej. el bus `/me/events` avisa que un subagente terminó).
83
+ */
84
+ refresh: () => Promise<void>;
71
85
  }
72
86
  /** El contexto de la app como `context` de AG-UI: `[{description, value}]`, `value` siempre texto. */
73
87
  export declare function toAgUiContext(ctx: unknown): RunAgentInput["context"];
package/dist/styles.css CHANGED
@@ -478,6 +478,9 @@
478
478
  .piloto .rotate-90 {
479
479
  rotate: 90deg;
480
480
  }
481
+ .piloto .rotate-180 {
482
+ rotate: 180deg;
483
+ }
481
484
  .piloto .transform {
482
485
  transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
483
486
  }
@@ -653,6 +656,10 @@
653
656
  border-style: var(--tw-border-style);
654
657
  border-width: 2px;
655
658
  }
659
+ .piloto .border-t {
660
+ border-top-style: var(--tw-border-style);
661
+ border-top-width: 1px;
662
+ }
656
663
  .piloto .border-b-2 {
657
664
  border-bottom-style: var(--tw-border-style);
658
665
  border-bottom-width: 2px;
@@ -843,6 +850,12 @@
843
850
  background-color: color-mix(in oklab, var(--color-sidebar) 80%, transparent);
844
851
  }
845
852
  }
853
+ .piloto .bg-text-danger {
854
+ background-color: var(--color-text-danger);
855
+ }
856
+ .piloto .bg-text-disabled {
857
+ background-color: var(--color-text-disabled);
858
+ }
846
859
  .piloto .bg-text-primary {
847
860
  background-color: var(--color-text-primary);
848
861
  }
@@ -852,6 +865,9 @@
852
865
  background-color: color-mix(in oklab, var(--color-text-secondary) 70%, transparent);
853
866
  }
854
867
  }
868
+ .piloto .bg-text-tertiary {
869
+ background-color: var(--color-text-tertiary);
870
+ }
855
871
  .piloto .bg-transparent {
856
872
  background-color: transparent;
857
873
  }
@@ -1131,6 +1147,9 @@
1131
1147
  .piloto .opacity-0 {
1132
1148
  opacity: 0%;
1133
1149
  }
1150
+ .piloto .opacity-60 {
1151
+ opacity: 60%;
1152
+ }
1134
1153
  .piloto .opacity-70 {
1135
1154
  opacity: 70%;
1136
1155
  }
@@ -1375,6 +1394,9 @@
1375
1394
  .piloto .hover\:text-foreground:hover {
1376
1395
  color: var(--color-foreground);
1377
1396
  }
1397
+ .piloto .hover\:text-text-danger:hover {
1398
+ color: var(--color-text-danger);
1399
+ }
1378
1400
  .piloto .hover\:text-text-primary:hover {
1379
1401
  color: var(--color-text-primary);
1380
1402
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghostykit",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Ghosty Autopilot \u2014 the autopilot chat for your React app, on AG-UI: works with LangGraph, Mastra, CrewAI or any AG-UI agent. Streaming text, agent steps, human approval cards (interrupt/resume), attachments and voice.",
5
5
  "type": "module",
6
6
  "main": "./dist/core/index.js",