ghostykit 0.6.2 → 0.6.4

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];
@@ -192,7 +193,7 @@ function toolLabel(t, v) {
192
193
  if (str(x.title)) patch.label = str(x.title);
193
194
  if (str(x.kind)) patch.kind = str(x.kind);
194
195
  if (str(x.path)) patch.path = str(x.path);
195
- if (str(x.detail)) patch.detail = str(x.detail);
196
+ if (str(x.detail)) Object.assign(patch, { detail: str(x.detail), detailFromServer: true });
196
197
  t.tools = (t.tools ?? []).map((y) => y.id === x.toolCallId ? { ...y, ...patch } : y);
197
198
  }
198
199
  function applyStoredCustom(t, m) {
@@ -258,7 +259,8 @@ function applySnapshot(s, messages) {
258
259
  const tools = t.tools?.map((x) => {
259
260
  const prev = o?.role === t.role ? o.tools?.find((y) => y.id === x.id) : void 0;
260
261
  const status = x.status === "pending" && (prev?.status ?? (live ? "in_progress" : void 0));
261
- return { ...prev, ...x, ...status ? { status } : {} };
262
+ const keepDetail = prev?.detailFromServer && !x.detailFromServer ? { detail: prev.detail, detailFromServer: true } : {};
263
+ return { ...prev, ...x, ...status ? { status } : {}, ...keepDetail };
262
264
  });
263
265
  const base = tools ? { ...t, tools } : t;
264
266
  if (!o || o.role !== t.role) return base;
@@ -283,7 +285,8 @@ function applySnapshot(s, messages) {
283
285
  };
284
286
  }
285
287
  function reduceAgUiEvent(prev, e, now = Date.now()) {
286
- const s = prev.effects.length ? { ...prev, effects: [] } : prev;
288
+ let s = prev.effects.length ? { ...prev, effects: [] } : prev;
289
+ if (s.stalled && PROGRESS.has(e.type)) s = { ...s, stalled: null };
287
290
  switch (e.type) {
288
291
  case "RUN_STARTED":
289
292
  return { ...s, busy: true, error: null, step: null, runId: e.runId ?? s.runId };
@@ -323,6 +326,7 @@ function reduceAgUiEvent(prev, e, now = Date.now()) {
323
326
  }
324
327
  case "TOOL_CALL_END": {
325
328
  const known = s.turns[s.turns.length - 1]?.tools?.find((x) => x.id === e.toolCallId);
329
+ if (known?.detailFromServer) return s;
326
330
  return upsertTool(s, e.toolCallId, { detail: argsDetail(known?.args) });
327
331
  }
328
332
  case "TOOL_CALL_RESULT": {
@@ -355,6 +359,10 @@ function reduceAgUiEvent(prev, e, now = Date.now()) {
355
359
  case "CUSTOM":
356
360
  if (e.name === "ghosty.title") return { ...s, effects: [{ type: "title", title: e.value?.title }] };
357
361
  if (e.name === "ghosty.history") return { ...s, historyNext: toHistory(e.value) };
362
+ if (e.name === "ghosty.stalled") {
363
+ const message = e.value?.message;
364
+ return { ...s, stalled: typeof message === "string" && message ? { message } : {} };
365
+ }
358
366
  if (e.name === "ghosty.tool") {
359
367
  const v = e.value ?? {};
360
368
  if (typeof v.toolCallId !== "string") return s;
@@ -363,7 +371,7 @@ function reduceAgUiEvent(prev, e, now = Date.now()) {
363
371
  if (str(v.title)) patch.label = str(v.title);
364
372
  if (str(v.kind)) patch.kind = str(v.kind);
365
373
  if (str(v.path)) patch.path = str(v.path);
366
- if (str(v.detail)) patch.detail = str(v.detail);
374
+ if (str(v.detail)) Object.assign(patch, { detail: str(v.detail), detailFromServer: true });
367
375
  return upsertTool(s, v.toolCallId, patch);
368
376
  }
369
377
  if (e.name === "ghosty.replace") {
@@ -390,7 +398,7 @@ function toHistory(v) {
390
398
  return { hasMoreBefore: h.hasMoreBefore === true, ...before !== void 0 ? { before } : {} };
391
399
  }
392
400
  function finish(s0, e, now, failed = false) {
393
- let s = { ...s0, busy: false, step: null, openText: null };
401
+ let s = { ...s0, busy: false, step: null, openText: null, stalled: null };
394
402
  const outcome = e.outcome;
395
403
  const interrupted = outcome?.type === "interrupt" && Array.isArray(outcome.interrupts) && outcome.interrupts.length > 0;
396
404
  const clockOffset = typeof e.timestamp === "number" ? now - e.timestamp : 0;
@@ -414,7 +422,9 @@ function finish(s0, e, now, failed = false) {
414
422
  const seconds = t0 ? Math.max(1, Math.round((now - t0) / 1e3)) : void 0;
415
423
  const turns = [...s.turns];
416
424
  const last = turns[turns.length - 1];
417
- if (last?.role === "assistant") turns[turns.length - 1] = { ...last, ...seconds !== void 0 ? { seconds } : {}, ...usage ? { usage } : {} };
425
+ const tr = e.metadata?.truncated;
426
+ const truncated = tr ? { ...typeof tr.reason === "string" ? { reason: tr.reason } : {} } : void 0;
427
+ if (last?.role === "assistant") turns[turns.length - 1] = { ...last, ...seconds !== void 0 ? { seconds } : {}, ...usage ? { usage } : {}, ...truncated ? { truncated } : {} };
418
428
  s = { ...s, turns, turnStartedAt: 0, ...usage ? { usage } : {} };
419
429
  if (!t0) return s;
420
430
  const effect = {
@@ -31,7 +31,7 @@ import {
31
31
  toPermission,
32
32
  toolName,
33
33
  toolNames
34
- } from "../chunks/chunk-ZJ34IR2Z.js";
34
+ } from "../chunks/chunk-LPPND5ME.js";
35
35
  import {
36
36
  applyReferences,
37
37
  cleanUserText,
@@ -23,6 +23,8 @@ export interface ToolEntry {
23
23
  path?: string;
24
24
  /** Los argumentos, en corto («job: purge-notifications»). */
25
25
  detail?: string;
26
+ /** `detail` lo puso el servidor (`ghosty.tool`): ya no se deriva de los argumentos. */
27
+ detailFromServer?: boolean;
26
28
  /** Los argumentos en JSON tal como llegaron (TOOL_CALL_ARGS). */
27
29
  args?: string;
28
30
  }
@@ -95,6 +97,10 @@ export interface Turn {
95
97
  usage?: Usage;
96
98
  /** Lo que el usuario decidió en las tarjetas de este turno (sólo en vivo). */
97
99
  decisions?: Decision[];
100
+ /** El servidor recortó la respuesta (`RUN_FINISHED.metadata.truncated`); `reason` si la dio. */
101
+ truncated?: {
102
+ reason?: string;
103
+ };
98
104
  /** Los `CUSTOM` del turno (p. ej. `ghosty.delivery`), para `renderDeliveries`. Del snapshot salen
99
105
  * del campo `custom` de cada mensaje del asistente. */
100
106
  custom?: CustomEntry[];
@@ -171,6 +177,10 @@ export interface PilotState {
171
177
  runId: string;
172
178
  reason?: string;
173
179
  } | null;
180
+ /** El agente se atoró y el servidor sigue intentando (`CUSTOM ghosty.stalled`); se quita con lo siguiente que llegue. */
181
+ stalled?: {
182
+ message?: string;
183
+ } | null;
174
184
  /** Mensajes en espera de que termine el run vivo (steer rechazado). */
175
185
  queued: QueuedMessage[];
176
186
  /** 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-ZJ34IR2Z.js";
34
+ } from "../chunks/chunk-LPPND5ME.js";
35
35
  import {
36
36
  PilotHttpError,
37
37
  createAgUiTransport,
@@ -896,6 +896,9 @@ var STRINGS_ES = {
896
896
  loadingOlder: "Cargando\u2026",
897
897
  continueRun: "Continuar",
898
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}`,
899
902
  queued: "Se manda cuando termine",
900
903
  subagentsWorking: (n) => n === 1 ? "1 agente trabajando" : `${n} agentes trabajando`,
901
904
  subagentsDone: (n) => n === 1 ? "1 agente termin\xF3" : `${n} agentes terminaron`,
@@ -971,6 +974,9 @@ var STRINGS_EN = {
971
974
  loadingOlder: "Loading\u2026",
972
975
  continueRun: "Continue",
973
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}`,
974
980
  queued: "Sends when this finishes",
975
981
  subagentsWorking: (n) => n === 1 ? "1 agent working" : `${n} agents working`,
976
982
  subagentsDone: (n) => n === 1 ? "1 agent finished" : `${n} agents finished`,
@@ -2388,7 +2394,7 @@ function usePilot(transport, opts = {}) {
2388
2394
  ctrl.current?.abort();
2389
2395
  const c = new AbortController();
2390
2396
  ctrl.current = c;
2391
- const steering = typeof input.forwardedProps?.steer === "string";
2397
+ const steering = input.forwardedProps?.steer !== void 0 && input.forwardedProps?.steer !== false;
2392
2398
  const userMsg = input.messages[0];
2393
2399
  if (!steering && (input.messages.length || input.resume?.length || input.forwardedProps?.continueRun)) liveRun.current = input.runId;
2394
2400
  optsRef.current.onSend?.(input);
@@ -2410,6 +2416,18 @@ function usePilot(transport, opts = {}) {
2410
2416
  } catch (err) {
2411
2417
  if (c.signal.aborted) return;
2412
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
+ }
2413
2431
  if (code === "nothing_to_continue") {
2414
2432
  liveRun.current = null;
2415
2433
  commit({ ...ref.current, busy: false, turnStartedAt: 0, cutRun: null, effects: [] });
@@ -2461,8 +2479,8 @@ function usePilot(transport, opts = {}) {
2461
2479
  async (text, attachments = [], _extra = {}) => {
2462
2480
  if (!transportRef.current) return;
2463
2481
  if (ref.current.busy) {
2464
- const live = liveRun.current ?? ref.current.runId;
2465
- 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;
2466
2484
  commit(beginSteer(ref.current, text));
2467
2485
  await runNow({ messages: [userMessage(text, [])], forwardedProps: { steer: live } });
2468
2486
  return;
@@ -2864,20 +2882,24 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2864
2882
  turns.map((turn, i) => {
2865
2883
  const isLast = i === turns.length - 1;
2866
2884
  const live = busy && isLast;
2867
- return /* @__PURE__ */ jsx10("div", { "data-turn": i - olderCount, children: turn.role === "user" ? /* @__PURE__ */ jsx10(UserBubble, { turn }) : /* @__PURE__ */ jsx10(
2868
- AgentBubble,
2869
- {
2870
- turn,
2871
- live,
2872
- onRetry: isLast && features.retry !== false ? pilot.retry : void 0,
2873
- onOption: isLast && !busy && features.replyOptions !== false ? (text) => send(text) : void 0,
2874
- showUsage: features.usage !== false,
2875
- showCopy: features.copy !== false,
2876
- showDuration: features.duration !== false,
2877
- renderDeliveries: slots.renderDeliveries
2878
- }
2879
- ) }, 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);
2880
2901
  }),
2902
+ busy && state.stalled && /* @__PURE__ */ jsx10("p", { className: "text-sm text-text-tertiary", children: state.stalled.message ?? t.stalled }),
2881
2903
  state.queued.map((q, i) => (
2882
2904
  // Esperan a que termine el run vivo (el servidor no los dejó entrar): salen solos.
2883
2905
  /* @__PURE__ */ jsxs8("div", { "data-queued": true, className: "flex flex-col items-end gap-1 opacity-60", children: [
@@ -2926,6 +2948,15 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2926
2948
  ] })
2927
2949
  ] })) }) });
2928
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
+ }
2929
2960
 
2930
2961
  // src/react/Subagents.tsx
2931
2962
  import { useState as useState10 } from "react";
@@ -62,6 +62,12 @@ export interface PilotStrings {
62
62
  continueRun: string;
63
63
  /** La franja, si el servidor no dio razón. */
64
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;
65
71
  /** Bajo un mensaje que espera a que termine el run vivo. */
66
72
  queued: string;
67
73
  /** «2 agentes trabajando». */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghostykit",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
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",