ghostykit 0.6.0 → 0.6.2

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.
@@ -184,6 +184,26 @@ function attachmentsOf(content) {
184
184
  }
185
185
  return out.length ? out : void 0;
186
186
  }
187
+ function toolLabel(t, v) {
188
+ const x = v ?? {};
189
+ if (typeof x.toolCallId !== "string") return;
190
+ const str = (y) => typeof y === "string" && y ? y : void 0;
191
+ const patch = {};
192
+ if (str(x.title)) patch.label = str(x.title);
193
+ if (str(x.kind)) patch.kind = str(x.kind);
194
+ if (str(x.path)) patch.path = str(x.path);
195
+ if (str(x.detail)) patch.detail = str(x.detail);
196
+ t.tools = (t.tools ?? []).map((y) => y.id === x.toolCallId ? { ...y, ...patch } : y);
197
+ }
198
+ function applyStoredCustom(t, m) {
199
+ const custom = m.metadata?.custom;
200
+ if (!Array.isArray(custom)) return;
201
+ for (const c of custom) {
202
+ if (!c || typeof c.name !== "string") continue;
203
+ if (c.name === "ghosty.tool") toolLabel(t, c.value);
204
+ else if (!NOT_IN_TURN.has(c.name)) t.custom = [...t.custom ?? [], c];
205
+ }
206
+ }
187
207
  function messagesToTurns(messages) {
188
208
  const turns = [];
189
209
  let cur = null;
@@ -201,8 +221,6 @@ function messagesToTurns(messages) {
201
221
  } else if (m.role === "assistant") {
202
222
  const t = assistant();
203
223
  if (!t.id && m.id) t.id = m.id;
204
- const custom = m.metadata?.custom;
205
- if (Array.isArray(custom) && custom.length) t.custom = [...t.custom ?? [], ...custom.filter((c) => c && typeof c.name === "string")];
206
224
  const text = m.content ?? "";
207
225
  if (text) t.text = t.text ? `${t.text}
208
226
 
@@ -211,10 +229,12 @@ ${text}` : text;
211
229
  t.tools = [...t.tools ?? [], { id: c.id, title: c.function.name, status: "pending", args: c.function.arguments, detail: argsDetail(c.function.arguments) }];
212
230
  if (t.text.trim() && t.cuts?.[t.cuts.length - 1] !== t.text.length) t.cuts = [...t.cuts ?? [], t.text.length];
213
231
  }
232
+ applyStoredCustom(t, m);
214
233
  } else if (m.role === "tool") {
215
234
  const t = assistant(m.id);
216
235
  const failed = Boolean(m.error);
217
236
  t.tools = (t.tools ?? []).map((x) => x.id === m.toolCallId ? { ...x, status: failed ? "failed" : "completed" } : x);
237
+ applyStoredCustom(t, m);
218
238
  } else if (m.role === "reasoning") {
219
239
  const t = assistant(m.id);
220
240
  t.thought = (t.thought ?? "") + textOf(m.content);
@@ -366,7 +386,8 @@ ${content}` : content };
366
386
  function toHistory(v) {
367
387
  if (!v || typeof v !== "object") return null;
368
388
  const h = v;
369
- return { hasMoreBefore: h.hasMoreBefore === true, ...typeof h.before === "string" ? { before: h.before } : {} };
389
+ const before = typeof h.before === "string" || typeof h.before === "number" ? String(h.before) : void 0;
390
+ return { hasMoreBefore: h.hasMoreBefore === true, ...before !== void 0 ? { before } : {} };
370
391
  }
371
392
  function finish(s0, e, now, failed = false) {
372
393
  let s = { ...s0, busy: false, step: null, openText: null };
@@ -387,6 +408,8 @@ function finish(s0, e, now, failed = false) {
387
408
  { used: 0, size: 0, cost: 0 }
388
409
  ) : null;
389
410
  const usage = tokens || costUsd !== void 0 ? { ...tokens ?? { used: 0, size: 0, cost: 0 }, ...costUsd !== void 0 ? { cost: costUsd } : {} } : null;
411
+ const cut = e.metadata?.interrupted;
412
+ s = { ...s, cutRun: cut && typeof cut.runId === "string" ? { runId: cut.runId, ...typeof cut.reason === "string" && cut.reason ? { reason: cut.reason } : {} } : null };
390
413
  const t0 = s.turnStartedAt;
391
414
  const seconds = t0 ? Math.max(1, Math.round((now - t0) / 1e3)) : void 0;
392
415
  const turns = [...s.turns];
@@ -406,7 +429,7 @@ function finish(s0, e, now, failed = false) {
406
429
  }
407
430
  function beginSend(prev, text, attachments = [], now = Date.now()) {
408
431
  const turns = text || attachments.length ? [...prev.turns, { role: "user", text, ...attachments.length ? { attachments } : {} }] : prev.turns;
409
- return { ...prev, turns, busy: true, error: null, notice: null, turnStartedAt: now, effects: [{ type: "turn-start" }] };
432
+ return { ...prev, turns, busy: true, error: null, notice: null, cutRun: null, turnStartedAt: now, effects: [{ type: "turn-start" }] };
410
433
  }
411
434
  function beginSteer(prev, text) {
412
435
  return { ...prev, turns: [...prev.turns, { role: "user", text }], notice: null, openText: null, effects: [] };
@@ -439,7 +462,33 @@ function markSubagent(prev, taskId, status) {
439
462
  function prependHistory(prev, messages, page) {
440
463
  const have = new Set(prev.turns.map((t) => t.id).filter(Boolean));
441
464
  const older = messagesToTurns(messages).filter((t) => !t.id || !have.has(t.id));
442
- return { ...prev, turns: [...older, ...prev.turns], olderCount: prev.olderCount + older.length, history: page, effects: [] };
465
+ let turns = prev.turns;
466
+ const tail = older[older.length - 1];
467
+ const head = turns[0];
468
+ if (tail?.role === "assistant" && head?.role === "assistant") {
469
+ older[older.length - 1] = joinTurns(tail, head);
470
+ turns = turns.slice(1);
471
+ }
472
+ const olderCount = Math.max(0, prev.olderCount + older.length - (turns === prev.turns ? 0 : 1));
473
+ return { ...prev, turns: [...older, ...turns], olderCount, history: page ? { ...page, ...page.before !== void 0 ? { before: String(page.before) } : {} } : page, effects: [] };
474
+ }
475
+ function cutsOf(a, b, shift) {
476
+ const bridge = a.text.trim() && b.tools?.length && !b.cuts?.length ? [a.text.length] : [];
477
+ const cuts = [...a.cuts ?? [], ...bridge, ...(b.cuts ?? []).map((c) => c + shift)];
478
+ return cuts.length ? { cuts } : {};
479
+ }
480
+ function joinTurns(a, b) {
481
+ const sep = a.text && b.text ? "\n\n" : "";
482
+ const shift = a.text.length + sep.length;
483
+ return {
484
+ ...b,
485
+ ...a.id ? { id: a.id } : {},
486
+ text: a.text + sep + b.text,
487
+ ...a.tools || b.tools ? { tools: [...a.tools ?? [], ...b.tools ?? []] } : {},
488
+ ...cutsOf(a, b, shift),
489
+ ...a.custom || b.custom ? { custom: [...a.custom ?? [], ...b.custom ?? []] } : {},
490
+ ...a.thought || b.thought ? { thought: (a.thought ?? "") + (b.thought ?? "") } : {}
491
+ };
443
492
  }
444
493
  function sendFailed(prev, message) {
445
494
  return { ...prev, busy: false, turnStartedAt: 0, error: message, effects: [] };
@@ -31,7 +31,7 @@ import {
31
31
  toPermission,
32
32
  toolName,
33
33
  toolNames
34
- } from "../chunks/chunk-4XKQ6XAJ.js";
34
+ } from "../chunks/chunk-ZJ34IR2Z.js";
35
35
  import {
36
36
  applyReferences,
37
37
  cleanUserText,
@@ -163,6 +163,14 @@ export interface PilotState {
163
163
  olderCount: number;
164
164
  /** El `ghosty.history` que acompaña al snapshot que viene. */
165
165
  historyNext?: HistoryPage | null;
166
+ /**
167
+ * El último run se cortó del lado del servidor (deploy, caja caída): `RUN_FINISHED.metadata.interrupted`.
168
+ * `reason` ya viene en palabras de persona. Se ofrece «Continuar».
169
+ */
170
+ cutRun?: {
171
+ runId: string;
172
+ reason?: string;
173
+ } | null;
166
174
  /** Mensajes en espera de que termine el run vivo (steer rechazado). */
167
175
  queued: QueuedMessage[];
168
176
  /** Lo que estaba en espera cuando la persona detuvo: regresa a la caja (`n` cambia en cada vez). */
@@ -31,7 +31,7 @@ import {
31
31
  toPermission,
32
32
  toolName,
33
33
  toolNames
34
- } from "../chunks/chunk-4XKQ6XAJ.js";
34
+ } from "../chunks/chunk-ZJ34IR2Z.js";
35
35
  import {
36
36
  PilotHttpError,
37
37
  createAgUiTransport,
@@ -894,6 +894,8 @@ var STRINGS_ES = {
894
894
  steps: { waking: "Despertando al agente\u2026", session: "Preparando la sesi\xF3n\u2026", compacting: "Resumiendo la conversaci\xF3n\u2026" },
895
895
  loadOlder: "Cargar mensajes anteriores",
896
896
  loadingOlder: "Cargando\u2026",
897
+ continueRun: "Continuar",
898
+ runCut: "La respuesta se cort\xF3 antes de terminar.",
897
899
  queued: "Se manda cuando termine",
898
900
  subagentsWorking: (n) => n === 1 ? "1 agente trabajando" : `${n} agentes trabajando`,
899
901
  subagentsDone: (n) => n === 1 ? "1 agente termin\xF3" : `${n} agentes terminaron`,
@@ -967,6 +969,8 @@ var STRINGS_EN = {
967
969
  steps: { waking: "Waking the agent\u2026", session: "Starting the session\u2026", compacting: "Compacting the conversation\u2026" },
968
970
  loadOlder: "Load earlier messages",
969
971
  loadingOlder: "Loading\u2026",
972
+ continueRun: "Continue",
973
+ runCut: "The answer was cut off before it finished.",
970
974
  queued: "Sends when this finishes",
971
975
  subagentsWorking: (n) => n === 1 ? "1 agent working" : `${n} agents working`,
972
976
  subagentsDone: (n) => n === 1 ? "1 agent finished" : `${n} agents finished`,
@@ -2386,7 +2390,7 @@ function usePilot(transport, opts = {}) {
2386
2390
  ctrl.current = c;
2387
2391
  const steering = typeof input.forwardedProps?.steer === "string";
2388
2392
  const userMsg = input.messages[0];
2389
- if (!steering && (input.messages.length || input.resume?.length)) liveRun.current = input.runId;
2393
+ if (!steering && (input.messages.length || input.resume?.length || input.forwardedProps?.continueRun)) liveRun.current = input.runId;
2390
2394
  optsRef.current.onSend?.(input);
2391
2395
  let ended = false;
2392
2396
  try {
@@ -2406,6 +2410,11 @@ function usePilot(transport, opts = {}) {
2406
2410
  } catch (err) {
2407
2411
  if (c.signal.aborted) return;
2408
2412
  const code = err instanceof PilotHttpError ? err.code : void 0;
2413
+ if (code === "nothing_to_continue") {
2414
+ liveRun.current = null;
2415
+ commit({ ...ref.current, busy: false, turnStartedAt: 0, cutRun: null, effects: [] });
2416
+ return;
2417
+ }
2409
2418
  if ((code === "run_active" || code === "interrupt_pending") && !reopening) {
2410
2419
  if (userMsg) {
2411
2420
  const { text, attachments } = fromUserMessage(userMsg.content);
@@ -2571,10 +2580,16 @@ function usePilot(transport, opts = {}) {
2571
2580
  // eslint-disable-next-line react-hooks/exhaustive-deps
2572
2581
  [commit, runNow]
2573
2582
  );
2583
+ const continueRun = useCallback2(async () => {
2584
+ const cut = ref.current.cutRun;
2585
+ if (!cut || ref.current.busy || !transportRef.current) return;
2586
+ commit(beginSend(ref.current, "", []));
2587
+ await runNow({ messages: [], forwardedProps: { continueRun: cut.runId } });
2588
+ }, [commit, runNow]);
2574
2589
  const refresh = useCallback2(() => runNow({ messages: [] }, true), [runNow]);
2575
2590
  const canSteer = Boolean(transport) && (opts.steer ?? state.agentState.caps?.steer === true);
2576
2591
  const subagents = useMemo3(() => subagentsOf(state.agentState), [state.agentState]);
2577
- return { state, threadId, send, stop, retry, respondPermission, recordDecision, expirePermission, canSteer, loadOlder, loadingOlder, subagents, stopSubagent, refresh };
2592
+ return { state, threadId, send, stop, retry, respondPermission, recordDecision, expirePermission, canSteer, loadOlder, loadingOlder, subagents, stopSubagent, refresh, continueRun };
2578
2593
  }
2579
2594
 
2580
2595
  // src/react/Piloto.tsx
@@ -2893,6 +2908,19 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2893
2908
  children: /* @__PURE__ */ jsx10(ArrowDown, { className: "h-4 w-4" })
2894
2909
  }
2895
2910
  ),
2911
+ state.cutRun && !busy && // El servidor cortó la respuesta (deploy, caja caída): la persona decide si sigue.
2912
+ /* @__PURE__ */ jsxs8("div", { "data-cut-run": true, role: "status", className: "mb-2 flex items-center gap-3 rounded-2xl border border-border-primary bg-background-secondary px-3 py-2 text-sm text-text-secondary", children: [
2913
+ /* @__PURE__ */ jsx10("span", { className: "min-w-0 flex-1", children: state.cutRun.reason ?? t.runCut }),
2914
+ /* @__PURE__ */ jsx10(
2915
+ "button",
2916
+ {
2917
+ type: "button",
2918
+ onClick: () => void pilot.continueRun(),
2919
+ className: "shrink-0 rounded-full bg-brand-500 px-3 py-1 text-sm font-medium text-white transition hover:bg-brand-600",
2920
+ children: t.continueRun
2921
+ }
2922
+ )
2923
+ ] }),
2896
2924
  aboveComposer,
2897
2925
  composer
2898
2926
  ] })
@@ -58,6 +58,10 @@ export interface PilotStrings {
58
58
  steps: Record<string, string>;
59
59
  loadOlder: string;
60
60
  loadingOlder: string;
61
+ /** El botón de la franja de un run cortado por el servidor (mismo rótulo en iOS y Android). */
62
+ continueRun: string;
63
+ /** La franja, si el servidor no dio razón. */
64
+ runCut: string;
61
65
  /** Bajo un mensaje que espera a que termine el run vivo. */
62
66
  queued: string;
63
67
  /** «2 agentes trabajando». */
@@ -82,6 +82,8 @@ export interface Pilot {
82
82
  * por fuera que algo cambió (p. ej. el bus `/me/events` avisa que un subagente terminó).
83
83
  */
84
84
  refresh: () => Promise<void>;
85
+ /** Continúa el run que el servidor cortó (`state.cutRun`): `forwardedProps.continueRun`. */
86
+ continueRun: () => Promise<void>;
85
87
  }
86
88
  /** El contexto de la app como `context` de AG-UI: `[{description, value}]`, `value` siempre texto. */
87
89
  export declare function toAgUiContext(ctx: unknown): RunAgentInput["context"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghostykit",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
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",