ghostykit 0.6.1 → 0.6.3

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.
@@ -116,7 +116,8 @@ function createPilotState(turns = []) {
116
116
  effects: []
117
117
  };
118
118
  }
119
- var NOT_IN_TURN = /* @__PURE__ */ new Set(["ghosty.title", "ghosty.injected", "ghosty.history", "ghosty.tool", "ghosty.replace", "ghosty.page"]);
119
+ var NOT_IN_TURN = /* @__PURE__ */ new Set(["ghosty.title", "ghosty.injected", "ghosty.history", "ghosty.tool", "ghosty.replace", "ghosty.page", "ghosty.stalled"]);
120
+ var PROGRESS = /* @__PURE__ */ new Set(["TEXT_MESSAGE_START", "TEXT_MESSAGE_CONTENT", "TEXT_MESSAGE_CHUNK", "TOOL_CALL_START", "TOOL_CALL_ARGS", "TOOL_CALL_CHUNK", "TOOL_CALL_RESULT", "REASONING_MESSAGE_CONTENT", "REASONING_MESSAGE_CHUNK", "RUN_STARTED"]);
120
121
  function patchAssistant(s, patch) {
121
122
  const turns = [...s.turns];
122
123
  const last = turns[turns.length - 1];
@@ -283,7 +284,8 @@ function applySnapshot(s, messages) {
283
284
  };
284
285
  }
285
286
  function reduceAgUiEvent(prev, e, now = Date.now()) {
286
- const s = prev.effects.length ? { ...prev, effects: [] } : prev;
287
+ let s = prev.effects.length ? { ...prev, effects: [] } : prev;
288
+ if (s.stalled && PROGRESS.has(e.type)) s = { ...s, stalled: null };
287
289
  switch (e.type) {
288
290
  case "RUN_STARTED":
289
291
  return { ...s, busy: true, error: null, step: null, runId: e.runId ?? s.runId };
@@ -355,6 +357,10 @@ function reduceAgUiEvent(prev, e, now = Date.now()) {
355
357
  case "CUSTOM":
356
358
  if (e.name === "ghosty.title") return { ...s, effects: [{ type: "title", title: e.value?.title }] };
357
359
  if (e.name === "ghosty.history") return { ...s, historyNext: toHistory(e.value) };
360
+ if (e.name === "ghosty.stalled") {
361
+ const message = e.value?.message;
362
+ return { ...s, stalled: typeof message === "string" && message ? { message } : {} };
363
+ }
358
364
  if (e.name === "ghosty.tool") {
359
365
  const v = e.value ?? {};
360
366
  if (typeof v.toolCallId !== "string") return s;
@@ -390,7 +396,7 @@ function toHistory(v) {
390
396
  return { hasMoreBefore: h.hasMoreBefore === true, ...before !== void 0 ? { before } : {} };
391
397
  }
392
398
  function finish(s0, e, now, failed = false) {
393
- let s = { ...s0, busy: false, step: null, openText: null };
399
+ let s = { ...s0, busy: false, step: null, openText: null, stalled: null };
394
400
  const outcome = e.outcome;
395
401
  const interrupted = outcome?.type === "interrupt" && Array.isArray(outcome.interrupts) && outcome.interrupts.length > 0;
396
402
  const clockOffset = typeof e.timestamp === "number" ? now - e.timestamp : 0;
@@ -408,11 +414,15 @@ function finish(s0, e, now, failed = false) {
408
414
  { used: 0, size: 0, cost: 0 }
409
415
  ) : null;
410
416
  const usage = tokens || costUsd !== void 0 ? { ...tokens ?? { used: 0, size: 0, cost: 0 }, ...costUsd !== void 0 ? { cost: costUsd } : {} } : null;
417
+ const cut = e.metadata?.interrupted;
418
+ s = { ...s, cutRun: cut && typeof cut.runId === "string" ? { runId: cut.runId, ...typeof cut.reason === "string" && cut.reason ? { reason: cut.reason } : {} } : null };
411
419
  const t0 = s.turnStartedAt;
412
420
  const seconds = t0 ? Math.max(1, Math.round((now - t0) / 1e3)) : void 0;
413
421
  const turns = [...s.turns];
414
422
  const last = turns[turns.length - 1];
415
- if (last?.role === "assistant") turns[turns.length - 1] = { ...last, ...seconds !== void 0 ? { seconds } : {}, ...usage ? { usage } : {} };
423
+ const tr = e.metadata?.truncated;
424
+ const truncated = tr ? { ...typeof tr.reason === "string" ? { reason: tr.reason } : {} } : void 0;
425
+ if (last?.role === "assistant") turns[turns.length - 1] = { ...last, ...seconds !== void 0 ? { seconds } : {}, ...usage ? { usage } : {}, ...truncated ? { truncated } : {} };
416
426
  s = { ...s, turns, turnStartedAt: 0, ...usage ? { usage } : {} };
417
427
  if (!t0) return s;
418
428
  const effect = {
@@ -427,7 +437,7 @@ function finish(s0, e, now, failed = false) {
427
437
  }
428
438
  function beginSend(prev, text, attachments = [], now = Date.now()) {
429
439
  const turns = text || attachments.length ? [...prev.turns, { role: "user", text, ...attachments.length ? { attachments } : {} }] : prev.turns;
430
- return { ...prev, turns, busy: true, error: null, notice: null, turnStartedAt: now, effects: [{ type: "turn-start" }] };
440
+ return { ...prev, turns, busy: true, error: null, notice: null, cutRun: null, turnStartedAt: now, effects: [{ type: "turn-start" }] };
431
441
  }
432
442
  function beginSteer(prev, text) {
433
443
  return { ...prev, turns: [...prev.turns, { role: "user", text }], notice: null, openText: null, effects: [] };
@@ -31,7 +31,7 @@ import {
31
31
  toPermission,
32
32
  toolName,
33
33
  toolNames
34
- } from "../chunks/chunk-T5PZC5NA.js";
34
+ } from "../chunks/chunk-LFFJLMDO.js";
35
35
  import {
36
36
  applyReferences,
37
37
  cleanUserText,
@@ -95,6 +95,10 @@ export interface Turn {
95
95
  usage?: Usage;
96
96
  /** Lo que el usuario decidió en las tarjetas de este turno (sólo en vivo). */
97
97
  decisions?: Decision[];
98
+ /** El servidor recortó la respuesta (`RUN_FINISHED.metadata.truncated`); `reason` si la dio. */
99
+ truncated?: {
100
+ reason?: string;
101
+ };
98
102
  /** Los `CUSTOM` del turno (p. ej. `ghosty.delivery`), para `renderDeliveries`. Del snapshot salen
99
103
  * del campo `custom` de cada mensaje del asistente. */
100
104
  custom?: CustomEntry[];
@@ -163,6 +167,18 @@ export interface PilotState {
163
167
  olderCount: number;
164
168
  /** El `ghosty.history` que acompaña al snapshot que viene. */
165
169
  historyNext?: HistoryPage | null;
170
+ /**
171
+ * El último run se cortó del lado del servidor (deploy, caja caída): `RUN_FINISHED.metadata.interrupted`.
172
+ * `reason` ya viene en palabras de persona. Se ofrece «Continuar».
173
+ */
174
+ cutRun?: {
175
+ runId: string;
176
+ reason?: string;
177
+ } | null;
178
+ /** El agente se atoró y el servidor sigue intentando (`CUSTOM ghosty.stalled`); se quita con lo siguiente que llegue. */
179
+ stalled?: {
180
+ message?: string;
181
+ } | null;
166
182
  /** Mensajes en espera de que termine el run vivo (steer rechazado). */
167
183
  queued: QueuedMessage[];
168
184
  /** 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-T5PZC5NA.js";
34
+ } from "../chunks/chunk-LFFJLMDO.js";
35
35
  import {
36
36
  PilotHttpError,
37
37
  createAgUiTransport,
@@ -894,6 +894,11 @@ 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.",
899
+ stalled: "Se ator\xF3, sigo intentando\u2026",
900
+ truncated: "La respuesta qued\xF3 incompleta.",
901
+ attachmentFailed: (name, reason) => reason ? `No pude leer ${name}: ${reason}` : `No pude leer ${name}`,
897
902
  queued: "Se manda cuando termine",
898
903
  subagentsWorking: (n) => n === 1 ? "1 agente trabajando" : `${n} agentes trabajando`,
899
904
  subagentsDone: (n) => n === 1 ? "1 agente termin\xF3" : `${n} agentes terminaron`,
@@ -967,6 +972,11 @@ var STRINGS_EN = {
967
972
  steps: { waking: "Waking the agent\u2026", session: "Starting the session\u2026", compacting: "Compacting the conversation\u2026" },
968
973
  loadOlder: "Load earlier messages",
969
974
  loadingOlder: "Loading\u2026",
975
+ continueRun: "Continue",
976
+ runCut: "The answer was cut off before it finished.",
977
+ stalled: "Got stuck, still trying\u2026",
978
+ truncated: "The answer is incomplete.",
979
+ attachmentFailed: (name, reason) => reason ? `Couldn't read ${name}: ${reason}` : `Couldn't read ${name}`,
970
980
  queued: "Sends when this finishes",
971
981
  subagentsWorking: (n) => n === 1 ? "1 agent working" : `${n} agents working`,
972
982
  subagentsDone: (n) => n === 1 ? "1 agent finished" : `${n} agents finished`,
@@ -2384,9 +2394,9 @@ function usePilot(transport, opts = {}) {
2384
2394
  ctrl.current?.abort();
2385
2395
  const c = new AbortController();
2386
2396
  ctrl.current = c;
2387
- const steering = typeof input.forwardedProps?.steer === "string";
2397
+ const steering = input.forwardedProps?.steer !== void 0 && input.forwardedProps?.steer !== false;
2388
2398
  const userMsg = input.messages[0];
2389
- if (!steering && (input.messages.length || input.resume?.length)) liveRun.current = input.runId;
2399
+ if (!steering && (input.messages.length || input.resume?.length || input.forwardedProps?.continueRun)) liveRun.current = input.runId;
2390
2400
  optsRef.current.onSend?.(input);
2391
2401
  let ended = false;
2392
2402
  try {
@@ -2406,6 +2416,23 @@ function usePilot(transport, opts = {}) {
2406
2416
  } catch (err) {
2407
2417
  if (c.signal.aborted) return;
2408
2418
  const code = err instanceof PilotHttpError ? err.code : void 0;
2419
+ if (code === "run_not_live" && steering && userMsg) {
2420
+ const { text } = fromUserMessage(userMsg.content);
2421
+ let s = ref.current;
2422
+ for (let i = s.turns.length - 1; i >= 0; i--) {
2423
+ if (s.turns[i].role === "user" && s.turns[i].text === text) {
2424
+ s = { ...s, turns: [...s.turns.slice(0, i), ...s.turns.slice(i + 1)] };
2425
+ break;
2426
+ }
2427
+ }
2428
+ commit({ ...s, busy: false, turnStartedAt: 0, effects: [] });
2429
+ return sendNew(text, []);
2430
+ }
2431
+ if (code === "nothing_to_continue") {
2432
+ liveRun.current = null;
2433
+ commit({ ...ref.current, busy: false, turnStartedAt: 0, cutRun: null, effects: [] });
2434
+ return;
2435
+ }
2409
2436
  if ((code === "run_active" || code === "interrupt_pending") && !reopening) {
2410
2437
  if (userMsg) {
2411
2438
  const { text, attachments } = fromUserMessage(userMsg.content);
@@ -2452,8 +2479,8 @@ function usePilot(transport, opts = {}) {
2452
2479
  async (text, attachments = [], _extra = {}) => {
2453
2480
  if (!transportRef.current) return;
2454
2481
  if (ref.current.busy) {
2455
- const live = liveRun.current ?? ref.current.runId;
2456
- if (!steerOn() || !live || !text.trim() || attachments.length) return;
2482
+ const live = liveRun.current ?? ref.current.runId ?? true;
2483
+ if (!steerOn() || !text.trim() || attachments.length) return;
2457
2484
  commit(beginSteer(ref.current, text));
2458
2485
  await runNow({ messages: [userMessage(text, [])], forwardedProps: { steer: live } });
2459
2486
  return;
@@ -2571,10 +2598,16 @@ function usePilot(transport, opts = {}) {
2571
2598
  // eslint-disable-next-line react-hooks/exhaustive-deps
2572
2599
  [commit, runNow]
2573
2600
  );
2601
+ const continueRun = useCallback2(async () => {
2602
+ const cut = ref.current.cutRun;
2603
+ if (!cut || ref.current.busy || !transportRef.current) return;
2604
+ commit(beginSend(ref.current, "", []));
2605
+ await runNow({ messages: [], forwardedProps: { continueRun: cut.runId } });
2606
+ }, [commit, runNow]);
2574
2607
  const refresh = useCallback2(() => runNow({ messages: [] }, true), [runNow]);
2575
2608
  const canSteer = Boolean(transport) && (opts.steer ?? state.agentState.caps?.steer === true);
2576
2609
  const subagents = useMemo3(() => subagentsOf(state.agentState), [state.agentState]);
2577
- return { state, threadId, send, stop, retry, respondPermission, recordDecision, expirePermission, canSteer, loadOlder, loadingOlder, subagents, stopSubagent, refresh };
2610
+ return { state, threadId, send, stop, retry, respondPermission, recordDecision, expirePermission, canSteer, loadOlder, loadingOlder, subagents, stopSubagent, refresh, continueRun };
2578
2611
  }
2579
2612
 
2580
2613
  // src/react/Piloto.tsx
@@ -2849,20 +2882,24 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2849
2882
  turns.map((turn, i) => {
2850
2883
  const isLast = i === turns.length - 1;
2851
2884
  const live = busy && isLast;
2852
- return /* @__PURE__ */ jsx10("div", { "data-turn": i - olderCount, children: turn.role === "user" ? /* @__PURE__ */ jsx10(UserBubble, { turn }) : /* @__PURE__ */ jsx10(
2853
- AgentBubble,
2854
- {
2855
- turn,
2856
- live,
2857
- onRetry: isLast && features.retry !== false ? pilot.retry : void 0,
2858
- onOption: isLast && !busy && features.replyOptions !== false ? (text) => send(text) : void 0,
2859
- showUsage: features.usage !== false,
2860
- showCopy: features.copy !== false,
2861
- showDuration: features.duration !== false,
2862
- renderDeliveries: slots.renderDeliveries
2863
- }
2864
- ) }, i - olderCount);
2885
+ return /* @__PURE__ */ jsxs8("div", { "data-turn": i - olderCount, children: [
2886
+ turn.role === "user" ? /* @__PURE__ */ jsx10(UserBubble, { turn }) : /* @__PURE__ */ jsx10(
2887
+ AgentBubble,
2888
+ {
2889
+ turn,
2890
+ live,
2891
+ onRetry: isLast && features.retry !== false ? pilot.retry : void 0,
2892
+ onOption: isLast && !busy && features.replyOptions !== false ? (text) => send(text) : void 0,
2893
+ showUsage: features.usage !== false,
2894
+ showCopy: features.copy !== false,
2895
+ showDuration: features.duration !== false,
2896
+ renderDeliveries: slots.renderDeliveries
2897
+ }
2898
+ ),
2899
+ turn.role === "assistant" && /* @__PURE__ */ jsx10(TurnNotes, { turn })
2900
+ ] }, i - olderCount);
2865
2901
  }),
2902
+ busy && state.stalled && /* @__PURE__ */ jsx10("p", { className: "text-sm text-text-tertiary", children: state.stalled.message ?? t.stalled }),
2866
2903
  state.queued.map((q, i) => (
2867
2904
  // Esperan a que termine el run vivo (el servidor no los dejó entrar): salen solos.
2868
2905
  /* @__PURE__ */ jsxs8("div", { "data-queued": true, className: "flex flex-col items-end gap-1 opacity-60", children: [
@@ -2893,11 +2930,33 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2893
2930
  children: /* @__PURE__ */ jsx10(ArrowDown, { className: "h-4 w-4" })
2894
2931
  }
2895
2932
  ),
2933
+ state.cutRun && !busy && // El servidor cortó la respuesta (deploy, caja caída): la persona decide si sigue.
2934
+ /* @__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: [
2935
+ /* @__PURE__ */ jsx10("span", { className: "min-w-0 flex-1", children: state.cutRun.reason ?? t.runCut }),
2936
+ /* @__PURE__ */ jsx10(
2937
+ "button",
2938
+ {
2939
+ type: "button",
2940
+ onClick: () => void pilot.continueRun(),
2941
+ className: "shrink-0 rounded-full bg-brand-500 px-3 py-1 text-sm font-medium text-white transition hover:bg-brand-600",
2942
+ children: t.continueRun
2943
+ }
2944
+ )
2945
+ ] }),
2896
2946
  aboveComposer,
2897
2947
  composer
2898
2948
  ] })
2899
2949
  ] })) }) });
2900
2950
  });
2951
+ function TurnNotes({ turn }) {
2952
+ const { strings: t } = usePiloto();
2953
+ const failed = (turn.custom ?? []).filter((c) => c.name === "ghosty.attachments_failed").flatMap((c) => (c.value?.files ?? []).filter((f) => f?.name));
2954
+ if (!failed.length && !turn.truncated) return null;
2955
+ return /* @__PURE__ */ jsxs8("div", { "data-turn-notes": true, className: "mt-1 flex flex-col gap-0.5 text-xs text-text-tertiary", children: [
2956
+ failed.map((f, i) => /* @__PURE__ */ jsx10("p", { children: t.attachmentFailed(f.name, f.reason) }, i)),
2957
+ turn.truncated && /* @__PURE__ */ jsx10("p", { children: turn.truncated.reason ?? t.truncated })
2958
+ ] });
2959
+ }
2901
2960
 
2902
2961
  // src/react/Subagents.tsx
2903
2962
  import { useState as useState10 } from "react";
@@ -58,6 +58,16 @@ 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;
65
+ /** `ghosty.stalled` sin mensaje propio. */
66
+ stalled: string;
67
+ /** Bajo una respuesta recortada por el servidor. */
68
+ truncated: string;
69
+ /** Un adjunto que el agente no pudo leer: nombre y razón. */
70
+ attachmentFailed: (name: string, reason?: string) => string;
61
71
  /** Bajo un mensaje que espera a que termine el run vivo. */
62
72
  queued: string;
63
73
  /** «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.1",
3
+ "version": "0.6.3",
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",