ghostykit 0.4.0 → 0.5.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
@@ -109,10 +109,13 @@ ref.current?.send("Why did this fail?"); // ask on the user'
109
109
  ```
110
110
 
111
111
  Other options: `strings`, `toolIcons`, `formatUserText`, `markdown`, `features`
112
- (`attachments`, `retry`, `copy`, `usage`, `replyOptions`, `duration`), `references` /
113
- `onReferencesChange`, `voiceNoteText`, `placeholder`, `avatar`, `initialTurns`, and the callbacks
114
- `onTurnStart`, `onTitle`, `onEvent` (every raw AG-UI event: STATE_*, CUSTOM…), `onSend`. To read the state
115
- outside the chat, call `usePilot(transport)` yourself and pass the result as `pilot`. Every piece is
112
+ (`attachments`, `retry`, `copy`, `usage`, `replyOptions`, `duration`, `steer`), `references` /
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
118
+ and pass the result as `pilot`. Every piece is
116
119
  exported (`AgentBubble`, `AgentSteps`, `Composer`, `ConfirmCard`, `Markdown`, …) if you want to build
117
120
  your own layout.
118
121
 
@@ -65,10 +65,14 @@ function createAgUiTransport(opts) {
65
65
  throw new PilotHttpError(json?.message ?? json?.error ?? `Error ${res.status}`, res.status, json?.error, retryAfter);
66
66
  }
67
67
  if (!res.body) return;
68
+ const serverDate = Date.parse(res.headers.get("date") ?? "");
69
+ const skew = Number.isNaN(serverDate) ? null : serverDate - Date.now();
68
70
  await readSse(res.body, ({ data }) => {
69
71
  if (!data) return;
70
72
  try {
71
- onEvent(JSON.parse(data));
73
+ const event = JSON.parse(data);
74
+ if (skew !== null && typeof event.timestamp !== "number") event.timestamp = Date.now() + skew;
75
+ onEvent(event);
72
76
  } catch {
73
77
  }
74
78
  });
@@ -1,7 +1,120 @@
1
+ // src/core/jsonPatch.ts
2
+ function parsePath(path) {
3
+ if (path === "") return [];
4
+ if (!path.startsWith("/")) throw new Error(`bad path ${path}`);
5
+ return path.slice(1).split("/").map((k) => k.replace(/~1/g, "/").replace(/~0/g, "~"));
6
+ }
7
+ function get(doc, keys) {
8
+ let cur = doc;
9
+ for (const k of keys) {
10
+ if (Array.isArray(cur)) cur = cur[Number(k)];
11
+ else if (cur && typeof cur === "object") cur = cur[k];
12
+ else return void 0;
13
+ }
14
+ return cur;
15
+ }
16
+ function edit(doc, keys, fn) {
17
+ if (!keys.length) throw new Error("root");
18
+ const [k, ...rest] = keys;
19
+ if (!rest.length) return fn(doc, k);
20
+ if (Array.isArray(doc)) {
21
+ const i = Number(k);
22
+ if (!(i in doc)) throw new Error(`missing ${k}`);
23
+ const out = [...doc];
24
+ out[i] = edit(doc[i], rest, fn);
25
+ return out;
26
+ }
27
+ if (doc && typeof doc === "object" && k in doc) {
28
+ return { ...doc, [k]: edit(doc[k], rest, fn) };
29
+ }
30
+ throw new Error(`missing ${k}`);
31
+ }
32
+ function add(doc, keys, value) {
33
+ if (!keys.length) return value;
34
+ return edit(doc, keys, (parent, k) => {
35
+ if (Array.isArray(parent)) {
36
+ const i = k === "-" ? parent.length : Number(k);
37
+ if (!Number.isInteger(i) || i < 0 || i > parent.length) throw new Error(`index ${k}`);
38
+ const out = [...parent];
39
+ out.splice(i, 0, value);
40
+ return out;
41
+ }
42
+ if (parent && typeof parent === "object") return { ...parent, [k]: value };
43
+ throw new Error("not a container");
44
+ });
45
+ }
46
+ function remove(doc, keys) {
47
+ return edit(doc, keys, (parent, k) => {
48
+ if (Array.isArray(parent)) {
49
+ const i = Number(k);
50
+ if (!(i in parent)) throw new Error(`index ${k}`);
51
+ return parent.filter((_, j) => j !== i);
52
+ }
53
+ if (parent && typeof parent === "object" && k in parent) {
54
+ const { [k]: _, ...rest } = parent;
55
+ return rest;
56
+ }
57
+ throw new Error(`missing ${k}`);
58
+ });
59
+ }
60
+ function replace(doc, keys, value) {
61
+ if (!keys.length) return value;
62
+ return edit(doc, keys, (parent, k) => {
63
+ if (Array.isArray(parent)) {
64
+ const i = Number(k);
65
+ if (!(i in parent)) throw new Error(`index ${k}`);
66
+ const out = [...parent];
67
+ out[i] = value;
68
+ return out;
69
+ }
70
+ if (parent && typeof parent === "object" && k in parent) return { ...parent, [k]: value };
71
+ throw new Error(`missing ${k}`);
72
+ });
73
+ }
74
+ var equal = (a, b) => JSON.stringify(a) === JSON.stringify(b);
75
+ function applyJsonPatch(doc, ops) {
76
+ let cur = doc;
77
+ try {
78
+ for (const o of ops) {
79
+ const keys = parsePath(o.path);
80
+ if (o.op === "add") cur = add(cur, keys, o.value);
81
+ else if (o.op === "remove") cur = remove(cur, keys);
82
+ else if (o.op === "replace") cur = replace(cur, keys, o.value);
83
+ else if (o.op === "move" || o.op === "copy") {
84
+ const from = parsePath(o.from ?? "");
85
+ const v = get(cur, from);
86
+ if (v === void 0) throw new Error(`missing ${o.from}`);
87
+ if (o.op === "move") cur = remove(cur, from);
88
+ cur = add(cur, keys, v);
89
+ } else if (o.op === "test") {
90
+ if (!equal(get(cur, keys), o.value)) throw new Error(`test ${o.path}`);
91
+ } else throw new Error(`op ${o.op}`);
92
+ }
93
+ return { doc: cur, ok: true };
94
+ } catch {
95
+ return { doc, ok: false };
96
+ }
97
+ }
98
+
1
99
  // src/core/reducer.ts
2
100
  function createPilotState(turns = []) {
3
- return { turns, busy: false, error: null, notice: null, usage: null, permissions: [], turnStartedAt: 0, runId: null, effects: [] };
101
+ return {
102
+ turns,
103
+ busy: false,
104
+ error: null,
105
+ notice: null,
106
+ usage: null,
107
+ permissions: [],
108
+ turnStartedAt: 0,
109
+ runId: null,
110
+ agentState: {},
111
+ step: null,
112
+ history: null,
113
+ olderCount: 0,
114
+ effects: []
115
+ };
4
116
  }
117
+ var PROTOCOL_CUSTOM = /* @__PURE__ */ new Set(["ghosty.title", "ghosty.injected", "ghosty.history"]);
5
118
  function patchAssistant(s, patch) {
6
119
  const turns = [...s.turns];
7
120
  const last = turns[turns.length - 1];
@@ -36,7 +149,7 @@ function upsertTool(s, id, patch) {
36
149
  }
37
150
  var APPROVE = { optionId: "approve", name: "Aprobar", kind: "allow_once" };
38
151
  var REJECT = { optionId: "reject", name: "Rechazar", kind: "reject_once" };
39
- function toPermission(i, turns) {
152
+ function toPermission(i, turns, clockOffset = 0) {
40
153
  const meta = i.metadata ?? {};
41
154
  const toolName2 = (typeof meta.tool === "string" ? meta.tool : void 0) ?? turns.flatMap((t) => t.tools ?? []).find((x) => x.id === i.toolCallId)?.title;
42
155
  return {
@@ -46,9 +159,14 @@ function toPermission(i, turns) {
46
159
  ...i.toolCallId ? { toolCallId: i.toolCallId } : {},
47
160
  reason: i.reason,
48
161
  options: [REJECT, APPROVE],
49
- ...i.expiresAt ? { expiresAt: i.expiresAt } : {}
162
+ ...i.expiresAt ? { expiresAt: i.expiresAt, deadline: expiryMs(i.expiresAt) + clockOffset } : {}
50
163
  };
51
164
  }
165
+ function closeOpenTools(turns, status) {
166
+ return turns.map(
167
+ (t) => t.tools?.some((x) => x.status === "in_progress") ? { ...t, tools: t.tools.map((x) => x.status === "in_progress" ? { ...x, status } : x) } : t
168
+ );
169
+ }
52
170
  function textOf(content) {
53
171
  if (typeof content === "string") return content;
54
172
  if (Array.isArray(content)) return content.map((p) => p && typeof p === "object" && p.type === "text" ? p.text ?? "" : "").join("");
@@ -67,9 +185,9 @@ function attachmentsOf(content) {
67
185
  function messagesToTurns(messages) {
68
186
  const turns = [];
69
187
  let cur = null;
70
- const assistant = () => {
188
+ const assistant = (id) => {
71
189
  if (!cur) {
72
- cur = { role: "assistant", text: "" };
190
+ cur = { ...id ? { id } : {}, role: "assistant", text: "" };
73
191
  turns.push(cur);
74
192
  }
75
193
  return cur;
@@ -77,9 +195,12 @@ function messagesToTurns(messages) {
77
195
  for (const m of messages) {
78
196
  if (m.role === "user") {
79
197
  cur = null;
80
- turns.push({ role: "user", text: textOf(m.content), ...attachmentsOf(m.content) ? { attachments: attachmentsOf(m.content) } : {} });
198
+ turns.push({ ...m.id ? { id: m.id } : {}, role: "user", text: textOf(m.content), ...attachmentsOf(m.content) ? { attachments: attachmentsOf(m.content) } : {} });
81
199
  } else if (m.role === "assistant") {
82
200
  const t = assistant();
201
+ if (!t.id && m.id) t.id = m.id;
202
+ const custom = m.custom;
203
+ if (Array.isArray(custom) && custom.length) t.custom = [...t.custom ?? [], ...custom.filter((c) => c && typeof c.name === "string")];
83
204
  const text = m.content ?? "";
84
205
  if (text) t.text = t.text ? `${t.text}
85
206
 
@@ -89,11 +210,11 @@ ${text}` : text;
89
210
  if (t.text.trim() && t.cuts?.[t.cuts.length - 1] !== t.text.length) t.cuts = [...t.cuts ?? [], t.text.length];
90
211
  }
91
212
  } else if (m.role === "tool") {
92
- const t = assistant();
213
+ const t = assistant(m.id);
93
214
  const failed = Boolean(m.error);
94
215
  t.tools = (t.tools ?? []).map((x) => x.id === m.toolCallId ? { ...x, status: failed ? "failed" : "completed" } : x);
95
216
  } else if (m.role === "reasoning") {
96
- const t = assistant();
217
+ const t = assistant(m.id);
97
218
  t.thought = (t.thought ?? "") + textOf(m.content);
98
219
  }
99
220
  }
@@ -101,27 +222,47 @@ ${text}` : text;
101
222
  }
102
223
  function applySnapshot(s, messages) {
103
224
  const fresh = messagesToTurns(messages);
104
- const old = s.turns;
225
+ let prefix = [];
226
+ if (s.olderCount && fresh[0]?.id) {
227
+ const at = s.turns.findIndex((t) => t.id === fresh[0].id);
228
+ if (at >= 0) prefix = s.turns.slice(0, at);
229
+ }
230
+ const keepOlder = prefix.length > 0;
231
+ const old = keepOlder ? s.turns.slice(prefix.length) : s.turns.slice(s.olderCount);
232
+ const lastIndex = fresh.length - 1;
105
233
  const turns = fresh.map((t, i) => {
106
234
  const o = old[i];
107
- if (!o || o.role !== t.role) return t;
235
+ const live = s.busy && i === lastIndex && t.role === "assistant";
236
+ const tools = t.tools?.map((x) => {
237
+ const prev = o?.role === t.role ? o.tools?.find((y) => y.id === x.id) : void 0;
238
+ const status = x.status === "pending" && (prev?.status ?? (live ? "in_progress" : void 0));
239
+ return { ...prev, ...x, ...status ? { status } : {} };
240
+ });
241
+ const base = tools ? { ...t, tools } : t;
242
+ if (!o || o.role !== t.role) return base;
108
243
  return {
109
- ...t,
244
+ ...base,
110
245
  ...o.seconds !== void 0 ? { seconds: o.seconds } : {},
111
246
  ...o.usage ? { usage: o.usage } : {},
112
247
  ...o.decisions ? { decisions: o.decisions } : {},
113
248
  ...o.thought && !t.thought ? { thought: o.thought } : {},
114
249
  ...o.attachments && !t.attachments ? { attachments: o.attachments } : {},
115
- ...o.tools && t.tools ? { tools: t.tools.map((x) => ({ ...o.tools.find((y) => y.id === x.id), ...x })) } : {}
250
+ ...o.custom && !t.custom ? { custom: o.custom } : {}
116
251
  };
117
252
  });
118
- return { ...s, turns };
253
+ return {
254
+ ...s,
255
+ turns: [...prefix, ...turns],
256
+ olderCount: prefix.length,
257
+ history: keepOlder ? s.history : s.historyNext ?? null,
258
+ historyNext: void 0
259
+ };
119
260
  }
120
261
  function reduceAgUiEvent(prev, e, now = Date.now()) {
121
262
  const s = prev.effects.length ? { ...prev, effects: [] } : prev;
122
263
  switch (e.type) {
123
264
  case "RUN_STARTED":
124
- return { ...s, busy: true, error: null, runId: e.runId ?? s.runId };
265
+ return { ...s, busy: true, error: null, step: null, runId: e.runId ?? s.runId };
125
266
  case "TEXT_MESSAGE_START":
126
267
  if (e.role && e.role !== "assistant") return s;
127
268
  return patchAssistant(s, (t) => ({ ...t, text: t.text && !t.text.endsWith("\n") ? `${t.text}
@@ -164,18 +305,40 @@ function reduceAgUiEvent(prev, e, now = Date.now()) {
164
305
  return finish(s, e, now);
165
306
  case "RUN_ERROR":
166
307
  return finish({ ...s, error: typeof e.message === "string" ? e.message : "El agente fall\xF3." }, e, now, true);
308
+ case "STEP_STARTED":
309
+ return typeof e.stepName === "string" ? { ...s, step: e.stepName } : s;
310
+ case "STEP_FINISHED":
311
+ return s.step === e.stepName ? { ...s, step: null } : s;
312
+ case "STATE_SNAPSHOT": {
313
+ const agentState = e.snapshot && typeof e.snapshot === "object" ? e.snapshot : {};
314
+ return { ...s, agentState, effects: [{ type: "state", state: agentState }] };
315
+ }
316
+ case "STATE_DELTA": {
317
+ if (!Array.isArray(e.delta)) return s;
318
+ const { doc, ok } = applyJsonPatch(s.agentState, e.delta);
319
+ return ok ? { ...s, agentState: doc, effects: [{ type: "state", state: doc }] } : s;
320
+ }
167
321
  case "CUSTOM":
168
322
  if (e.name === "ghosty.title") return { ...s, effects: [{ type: "title", title: e.value?.title }] };
169
- return s;
323
+ if (e.name === "ghosty.history") return { ...s, historyNext: toHistory(e.value) };
324
+ if (typeof e.name !== "string" || PROTOCOL_CUSTOM.has(e.name)) return s;
325
+ return patchAssistant(s, (t) => ({ ...t, custom: [...t.custom ?? [], { name: e.name, value: e.value, ...typeof e.timestamp === "number" ? { timestamp: e.timestamp } : {} }] }));
170
326
  default:
171
327
  return s;
172
328
  }
173
329
  }
330
+ function toHistory(v) {
331
+ if (!v || typeof v !== "object") return null;
332
+ const h = v;
333
+ return { hasMoreBefore: h.hasMoreBefore === true, ...typeof h.before === "string" ? { before: h.before } : {} };
334
+ }
174
335
  function finish(s0, e, now, failed = false) {
175
- let s = { ...s0, busy: false };
336
+ let s = { ...s0, busy: false, step: null };
176
337
  const outcome = e.outcome;
177
338
  const interrupted = outcome?.type === "interrupt" && Array.isArray(outcome.interrupts) && outcome.interrupts.length > 0;
178
- s.permissions = interrupted ? outcome.interrupts.map((i) => toPermission(i, s.turns)) : [];
339
+ const clockOffset = typeof e.timestamp === "number" ? now - e.timestamp : 0;
340
+ s.permissions = interrupted ? outcome.interrupts.map((i) => toPermission(i, s.turns, clockOffset)) : [];
341
+ if (outcome?.type === "cancelled" || failed) s = { ...s, turns: closeOpenTools(s.turns, failed ? "failed" : "cancelled") };
179
342
  if (interrupted) {
180
343
  const waiting = new Set(s.permissions.map((p) => p.toolCallId).filter(Boolean));
181
344
  if (waiting.size) {
@@ -207,11 +370,19 @@ function beginSend(prev, text, attachments = [], now = Date.now()) {
207
370
  const turns = text || attachments.length ? [...prev.turns, { role: "user", text, ...attachments.length ? { attachments } : {} }] : prev.turns;
208
371
  return { ...prev, turns, busy: true, error: null, notice: null, turnStartedAt: now, effects: [{ type: "turn-start" }] };
209
372
  }
373
+ function beginSteer(prev, text) {
374
+ return { ...prev, turns: [...prev.turns, { role: "user", text }], notice: null, effects: [] };
375
+ }
376
+ function prependHistory(prev, messages, page) {
377
+ const have = new Set(prev.turns.map((t) => t.id).filter(Boolean));
378
+ const older = messagesToTurns(messages).filter((t) => !t.id || !have.has(t.id));
379
+ return { ...prev, turns: [...older, ...prev.turns], olderCount: prev.olderCount + older.length, history: page, effects: [] };
380
+ }
210
381
  function sendFailed(prev, message) {
211
382
  return { ...prev, busy: false, turnStartedAt: 0, error: message, effects: [] };
212
383
  }
213
384
  function stopTurn(prev) {
214
- return { ...prev, busy: false, turnStartedAt: 0, effects: [] };
385
+ return { ...prev, busy: false, step: null, turnStartedAt: 0, turns: closeOpenTools(prev.turns, "cancelled"), effects: [] };
215
386
  }
216
387
  function dropLastExchange(prev) {
217
388
  const turns = [...prev.turns];
@@ -311,7 +482,7 @@ function cleanReplyOption(t) {
311
482
  }
312
483
 
313
484
  // src/core/steps.ts
314
- var isRunning = (t) => t.status !== "completed" && t.status !== "failed";
485
+ var isRunning = (t) => t.status !== "completed" && t.status !== "failed" && t.status !== "cancelled";
315
486
  function toolNames(t) {
316
487
  const title = t.title ?? "";
317
488
  if (!title) return [];
@@ -444,12 +615,15 @@ var TOOL_VERBS_EN = {
444
615
  };
445
616
 
446
617
  export {
618
+ applyJsonPatch,
447
619
  createPilotState,
448
620
  argsDetail,
449
621
  toPermission,
450
622
  messagesToTurns,
451
623
  reduceAgUiEvent,
452
624
  beginSend,
625
+ beginSteer,
626
+ prependHistory,
453
627
  sendFailed,
454
628
  stopTurn,
455
629
  dropLastExchange,
@@ -1,5 +1,6 @@
1
1
  export * from "./types";
2
- export { argsDetail, beginSend, createPilotState, dropLastExchange, expiryMs, lastUserTurn, messagesToTurns, reduceAgUiEvent, resolvePermission, sendFailed, stopTurn, toPermission, } from "./reducer";
2
+ export { argsDetail, beginSend, beginSteer, createPilotState, dropLastExchange, expiryMs, lastUserTurn, messagesToTurns, prependHistory, reduceAgUiEvent, resolvePermission, sendFailed, stopTurn, toPermission, } from "./reducer";
3
+ export { applyJsonPatch, type PatchOp } from "./jsonPatch";
3
4
  export { isNarration, narrationToMarkdown, splitNarration } from "./narration";
4
5
  export { cleanReplyOption, extractReplyOptions, type ReplyOptions } from "./options";
5
6
  export { TOOL_VERBS_EN, TOOL_VERBS_ES, groupSteps, isRunning, stepLabel, summarizeTools, toolName, toolNames, type ToolVerb, type ToolVerbs, } from "./steps";
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  TOOL_VERBS_EN,
3
3
  TOOL_VERBS_ES,
4
+ applyJsonPatch,
4
5
  argsDetail,
5
6
  beginSend,
7
+ beginSteer,
6
8
  cleanReplyOption,
7
9
  createPilotState,
8
10
  dropLastExchange,
@@ -14,6 +16,7 @@ import {
14
16
  lastUserTurn,
15
17
  messagesToTurns,
16
18
  narrationToMarkdown,
19
+ prependHistory,
17
20
  reduceAgUiEvent,
18
21
  resolvePermission,
19
22
  sendFailed,
@@ -24,7 +27,7 @@ import {
24
27
  toPermission,
25
28
  toolName,
26
29
  toolNames
27
- } from "../chunks/chunk-HPTM2BCM.js";
30
+ } from "../chunks/chunk-VK54J63C.js";
28
31
  import {
29
32
  applyReferences,
30
33
  cleanUserText,
@@ -34,9 +37,11 @@ import {
34
37
  export {
35
38
  TOOL_VERBS_EN,
36
39
  TOOL_VERBS_ES,
40
+ applyJsonPatch,
37
41
  applyReferences,
38
42
  argsDetail,
39
43
  beginSend,
44
+ beginSteer,
40
45
  cleanReplyOption,
41
46
  cleanUserText,
42
47
  createPilotState,
@@ -50,6 +55,7 @@ export {
50
55
  lastUserTurn,
51
56
  messagesToTurns,
52
57
  narrationToMarkdown,
58
+ prependHistory,
53
59
  reduceAgUiEvent,
54
60
  resolvePermission,
55
61
  sendFailed,
@@ -0,0 +1,11 @@
1
+ export interface PatchOp {
2
+ op: "add" | "remove" | "replace" | "move" | "copy" | "test";
3
+ path: string;
4
+ value?: unknown;
5
+ from?: string;
6
+ }
7
+ /** Aplica `ops` en orden. Si una falla, regresa el documento original y `ok: false`. */
8
+ export declare function applyJsonPatch<T>(doc: T, ops: PatchOp[]): {
9
+ doc: T;
10
+ ok: boolean;
11
+ };
@@ -1,5 +1,5 @@
1
1
  import type { Interrupt, Message } from "@ag-ui/core";
2
- import type { Attachment, Decision, PermissionRequest, PilotState, Turn } from "./types";
2
+ import type { Attachment, Decision, HistoryPage, PermissionRequest, PilotState, Turn } from "./types";
3
3
  export declare function createPilotState(turns?: Turn[]): PilotState;
4
4
  type Ev = {
5
5
  type: string;
@@ -8,7 +8,7 @@ type Ev = {
8
8
  /** Los argumentos de una tool en corto, para el renglón del paso: «job: purge-notifications». */
9
9
  export declare function argsDetail(json?: string): string | undefined;
10
10
  /** Un `Interrupt` de AG-UI como tarjeta: título, herramienta (por su toolCallId) y vencimiento. */
11
- export declare function toPermission(i: Interrupt, turns: Turn[]): PermissionRequest;
11
+ export declare function toPermission(i: Interrupt, turns: Turn[], clockOffset?: number): PermissionRequest;
12
12
  /**
13
13
  * El hilo guardado (mensajes AG-UI) como turnos: cada mensaje del usuario abre uno; lo que sigue del
14
14
  * asistente y de las herramientas se junta en un turno, con sus pasos y su narración.
@@ -18,6 +18,16 @@ export declare function messagesToTurns(messages: Message[]): Turn[];
18
18
  export declare function reduceAgUiEvent(prev: PilotState, e: Ev, now?: number): PilotState;
19
19
  /** El usuario manda un mensaje (o contesta una tarjeta: `text` vacío, sin burbuja). */
20
20
  export declare function beginSend(prev: PilotState, text: string, attachments?: Attachment[], now?: number): PilotState;
21
+ /**
22
+ * Steer: un mensaje del usuario mientras el run sigue. Su burbuja entra en el hilo y lo que el agente
23
+ * diga después va en una respuesta nueva debajo; el run (y su reloj) siguen siendo los mismos.
24
+ */
25
+ export declare function beginSteer(prev: PilotState, text: string): PilotState;
26
+ /**
27
+ * Mensajes más viejos (`loadOlder`) delante del hilo. Las páginas empiezan en un mensaje del usuario;
28
+ * lo que ya se tenía (mismo id) no se repite.
29
+ */
30
+ export declare function prependHistory(prev: PilotState, messages: Message[], page: HistoryPage | null): PilotState;
21
31
  /** El servidor no aceptó el encargo (HTTP): se DICE, para que no se quede pensando. */
22
32
  export declare function sendFailed(prev: PilotState, message: string): PilotState;
23
33
  /** Detener: el cliente cortó la conexión (el servidor corta el run). */
@@ -15,7 +15,8 @@ export interface ToolEntry {
15
15
  /** El nombre de la herramienta (`toolCallName`): es la llave de los verbos de la app. */
16
16
  title?: string;
17
17
  kind?: string;
18
- /** `in_progress` mientras corre; `completed` | `failed` con su resultado; `pending` = espera aprobación. */
18
+ /** `in_progress` mientras corre; `completed` | `failed` con su resultado; `pending` = espera
19
+ * aprobación; `cancelled` = el run se detuvo antes de que terminara. */
19
20
  status?: string;
20
21
  path?: string;
21
22
  /** Los argumentos, en corto («job: purge-notifications»). */
@@ -37,7 +38,21 @@ export interface Decision {
37
38
  status: "approved" | "rejected" | "expired" | "cancelled";
38
39
  detail?: string;
39
40
  }
41
+ /** Un evento `CUSTOM` del turno (entregas de archivos, avisos propios de la app). */
42
+ export interface CustomEntry {
43
+ name: string;
44
+ value: unknown;
45
+ timestamp?: number;
46
+ }
47
+ /** Hay mensajes más viejos que los del snapshot (`CUSTOM ghosty.history`). */
48
+ export interface HistoryPage {
49
+ hasMoreBefore: boolean;
50
+ /** Cursor opaco del servidor: se le regresa a `loadOlder`. */
51
+ before?: string;
52
+ }
40
53
  export interface Turn {
54
+ /** El id del primer mensaje AG-UI del turno (sólo los que vienen de un snapshot). */
55
+ id?: string;
41
56
  role: "user" | "assistant";
42
57
  text: string;
43
58
  /** Cuánto tardó, en segundos (sólo los turnos vistos en vivo). */
@@ -50,6 +65,9 @@ export interface Turn {
50
65
  usage?: Usage;
51
66
  /** Lo que el usuario decidió en las tarjetas de este turno (sólo en vivo). */
52
67
  decisions?: Decision[];
68
+ /** Los `CUSTOM` del turno (p. ej. `ghosty.delivery`), para `renderDeliveries`. Del snapshot salen
69
+ * del campo `custom` de cada mensaje del asistente. */
70
+ custom?: CustomEntry[];
53
71
  }
54
72
  /** Una opción de la tarjeta de aprobación. */
55
73
  export interface PermissionOption {
@@ -67,8 +85,14 @@ export interface PermissionRequest {
67
85
  toolCallId?: string;
68
86
  reason?: string;
69
87
  options: PermissionOption[];
70
- /** Hasta cuándo se puede contestar (ISO). */
88
+ /** Hasta cuándo se puede contestar (ISO, reloj del SERVIDOR). */
71
89
  expiresAt?: string | number;
90
+ /**
91
+ * El mismo vencimiento en el reloj de ESTE navegador (epoch ms): `expiresAt` menos la hora del
92
+ * servidor al mandar el evento, más la hora local al recibirlo. Así un reloj local desfasado no
93
+ * adelanta ni atrasa la cuenta regresiva.
94
+ */
95
+ deadline?: number;
72
96
  }
73
97
  /** Lo que una transición pide hacer FUERA del estado (avisos a la app). */
74
98
  export type PilotEffect = {
@@ -83,6 +107,9 @@ export type PilotEffect = {
83
107
  } | {
84
108
  type: "title";
85
109
  title?: string;
110
+ } | {
111
+ type: "state";
112
+ state: Record<string, unknown>;
86
113
  };
87
114
  export interface PilotState {
88
115
  turns: Turn[];
@@ -96,6 +123,16 @@ export interface PilotState {
96
123
  /** Cuándo empezó el turno que ESTE cliente encargó (0 = ninguno). */
97
124
  turnStartedAt: number;
98
125
  runId: string | null;
126
+ /** El estado compartido del agente (`STATE_SNAPSHOT`, con `STATE_DELTA` aplicado encima). */
127
+ agentState: Record<string, unknown>;
128
+ /** El paso con nombre en curso (`STEP_STARTED`), p. ej. `waking`. */
129
+ step: string | null;
130
+ /** Si hay mensajes más viejos por cargar (`loadOlder`). */
131
+ history: HistoryPage | null;
132
+ /** Cuántos de `turns` llegaron con `loadOlder` (van antes del snapshot). */
133
+ olderCount: number;
134
+ /** El `ghosty.history` que acompaña al snapshot que viene. */
135
+ historyNext?: HistoryPage | null;
99
136
  /** Efectos de la ÚLTIMA transición. El hook los ejecuta y los descarta. */
100
137
  effects: PilotEffect[];
101
138
  }
@@ -19,6 +19,8 @@ export interface PilotoFeatures {
19
19
  replyOptions?: boolean;
20
20
  /** «· 12 s» junto a la frase de espera. Default: sí. */
21
21
  duration?: boolean;
22
+ /** Mandar con el run vivo (steer). Default: lo que diga el agente en `state.caps.steer`. */
23
+ steer?: boolean;
22
24
  }
23
25
  export interface PilotoSlots {
24
26
  /** Lo que la app pinte bajo cada respuesta (archivos, tarjetas propias). */
@@ -31,6 +33,8 @@ export interface PilotoSlots {
31
33
  headerActions?: ReactNode;
32
34
  /** Pegado arriba del composer (un aviso, un banner). */
33
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
38
  }
35
39
  export interface PilotoProps extends UsePilotOptions {
36
40
  transport: PilotTransport | null;
@@ -1,6 +1,8 @@
1
1
  /** Los tres puntitos de «escribiendo» (color y animación en CSS: `gc-typing`). */
2
2
  export declare function TypingDots(): import("react").JSX.Element;
3
- export declare function WaitLabel({ since, showDuration, }: {
3
+ export declare function WaitLabel({ since, showDuration, step, }: {
4
+ /** El paso con nombre en curso (`STEP_STARTED`): si `strings.steps` lo nombra, esa es la frase. */
5
+ step?: string | null;
4
6
  /** Desde que se MANDÓ el mensaje (no desde que se montó la etiqueta). */
5
7
  since?: number | null;
6
8
  showDuration?: boolean;
@@ -1,8 +1,10 @@
1
1
  import {
2
2
  TOOL_VERBS_EN,
3
3
  TOOL_VERBS_ES,
4
+ applyJsonPatch,
4
5
  argsDetail,
5
6
  beginSend,
7
+ beginSteer,
6
8
  cleanReplyOption,
7
9
  createPilotState,
8
10
  dropLastExchange,
@@ -14,6 +16,7 @@ import {
14
16
  lastUserTurn,
15
17
  messagesToTurns,
16
18
  narrationToMarkdown,
19
+ prependHistory,
17
20
  reduceAgUiEvent,
18
21
  resolvePermission,
19
22
  sendFailed,
@@ -24,12 +27,12 @@ import {
24
27
  toPermission,
25
28
  toolName,
26
29
  toolNames
27
- } from "../chunks/chunk-HPTM2BCM.js";
30
+ } from "../chunks/chunk-VK54J63C.js";
28
31
  import {
29
32
  PilotHttpError,
30
33
  createAgUiTransport,
31
34
  readSse
32
- } from "../chunks/chunk-DXFWACUL.js";
35
+ } from "../chunks/chunk-TMEHQZWL.js";
33
36
  import {
34
37
  applyReferences,
35
38
  cleanUserText,
@@ -884,6 +887,9 @@ var STRINGS_ES = {
884
887
  linkOpen: "Abrir",
885
888
  close: "Cerrar",
886
889
  goToLatest: "Ir al \xFAltimo mensaje",
890
+ steps: { waking: "Despertando al agente\u2026", session: "Preparando la sesi\xF3n\u2026" },
891
+ loadOlder: "Cargar mensajes anteriores",
892
+ loadingOlder: "Cargando\u2026",
887
893
  confirmTitle: "Necesito tu permiso para",
888
894
  approve: "Aprobar",
889
895
  reject: "Rechazar",
@@ -949,6 +955,9 @@ var STRINGS_EN = {
949
955
  linkOpen: "Open",
950
956
  close: "Close",
951
957
  goToLatest: "Go to latest message",
958
+ steps: { waking: "Waking the agent\u2026", session: "Starting the session\u2026" },
959
+ loadOlder: "Load earlier messages",
960
+ loadingOlder: "Loading\u2026",
952
961
  confirmTitle: "I need your permission to",
953
962
  approve: "Approve",
954
963
  reject: "Reject",
@@ -1009,7 +1018,7 @@ function ConfirmCard({
1009
1018
  const { strings: t } = usePiloto();
1010
1019
  const [pending, setPending] = useState(null);
1011
1020
  const [stamp, setStamp] = useState(null);
1012
- const time = useRemaining(request.expiresAt);
1021
+ const time = useRemaining(request.deadline ?? request.expiresAt);
1013
1022
  const expired = time !== null && time.left <= 0;
1014
1023
  const urgent = time !== null && !expired && time.left < 6e4;
1015
1024
  useEffect(() => {
@@ -1448,7 +1457,10 @@ function StepLine({ tool, times, live }) {
1448
1457
  const spins = isRunning(tool) && live;
1449
1458
  const label = stepLabel(tool, toolVerbs, t.step);
1450
1459
  return /* @__PURE__ */ jsxs3("li", { className: "gs-paso-in flex min-w-0 items-center gap-2 text-[13px] leading-6 text-text-secondary", children: [
1451
- /* @__PURE__ */ jsx5("span", { className: "flex h-5 w-5 shrink-0 items-center justify-center", "aria-label": spins ? t.running : tool.status === "failed" ? t.failed : t.done, children: spins ? /* @__PURE__ */ jsx5(LoaderCircle, { className: "h-4 w-4 animate-spin text-text-info" }) : tool.status === "pending" ? (
1460
+ /* @__PURE__ */ jsx5("span", { className: "flex h-5 w-5 shrink-0 items-center justify-center", "aria-label": spins ? t.running : tool.status === "failed" ? t.failed : t.done, children: spins ? /* @__PURE__ */ jsx5(LoaderCircle, { className: "h-4 w-4 animate-spin text-text-info" }) : tool.status === "cancelled" ? (
1461
+ // El run se detuvo antes de que terminara.
1462
+ /* @__PURE__ */ jsx5(CircleSlash, { className: "h-4 w-4 text-text-tertiary" })
1463
+ ) : tool.status === "pending" ? (
1452
1464
  // Espera aprobación (el run se detuvo en un interrupt): ni gira ni está lista.
1453
1465
  /* @__PURE__ */ jsx5(Clock, { className: "h-4 w-4 text-text-tertiary" })
1454
1466
  ) : tool.status === "failed" ? /* @__PURE__ */ jsx5(CircleX, { className: "h-4 w-4 text-text-danger" }) : /* @__PURE__ */ jsx5(CircleCheck, { className: "h-4 w-4 text-text-success" }) }),
@@ -1470,6 +1482,7 @@ var STATUS = {
1470
1482
  pending: { Icon: Clock, className: "text-text-tertiary", key: "step" },
1471
1483
  in_progress: { Icon: LoaderCircle, className: "animate-spin text-text-info", key: "running" },
1472
1484
  completed: { Icon: CircleCheck, className: "text-text-success", key: "done" },
1485
+ cancelled: { Icon: CircleSlash, className: "text-text-tertiary", key: "step" },
1473
1486
  failed: { Icon: CircleX, className: "text-text-danger", key: "failed" }
1474
1487
  };
1475
1488
  function ToolRow({ tool }) {
@@ -2247,7 +2260,8 @@ function TypingDots() {
2247
2260
  }
2248
2261
  function WaitLabel({
2249
2262
  since,
2250
- showDuration = true
2263
+ showDuration = true,
2264
+ step
2251
2265
  }) {
2252
2266
  const { strings: t } = usePiloto();
2253
2267
  const [mounted] = useState7(() => Date.now());
@@ -2258,7 +2272,8 @@ function WaitLabel({
2258
2272
  }, []);
2259
2273
  const seconds = Math.max(0, Math.floor((now - (since || mounted)) / 1e3));
2260
2274
  const phrases = t.thinkingPhrases;
2261
- const what = phrases?.length ? seconds >= STILL_WORKING_S ? t.stillWorking : phrases[Math.floor(seconds / 4) % phrases.length] : t.howItGoes(seconds);
2275
+ const stepText = step ? t.steps?.[step] : void 0;
2276
+ const what = stepText ? stepText : phrases?.length ? seconds >= STILL_WORKING_S ? t.stillWorking : phrases[Math.floor(seconds / 4) % phrases.length] : t.howItGoes(seconds);
2262
2277
  return /* @__PURE__ */ jsx9("div", { className: "flex flex-col gap-0.5 text-sm text-text-secondary", children: /* @__PURE__ */ jsxs7("p", { className: "flex items-center", children: [
2263
2278
  /* @__PURE__ */ jsx9(AnimatePresence2, { mode: "wait", initial: false, children: /* @__PURE__ */ jsx9(
2264
2279
  motion2.span,
@@ -2325,6 +2340,7 @@ function usePilot(transport, opts = {}) {
2325
2340
  if (e.type === "turn-start") o.onTurnStart?.();
2326
2341
  else if (e.type === "turn-done") o.onTurnDone?.({ text: e.text, runId: e.runId, seconds: e.seconds, error: e.error, interrupted: e.interrupted });
2327
2342
  else if (e.type === "title") o.onTitle?.(e.title);
2343
+ else if (e.type === "state") o.onState?.(e.state);
2328
2344
  }
2329
2345
  }, []);
2330
2346
  const runNow = useCallback2(
@@ -2344,7 +2360,8 @@ function usePilot(transport, opts = {}) {
2344
2360
  ctrl.current?.abort();
2345
2361
  const c = new AbortController();
2346
2362
  ctrl.current = c;
2347
- if (input.messages.length || input.resume?.length) liveRun.current = input.runId;
2363
+ const steering = typeof input.forwardedProps?.steer === "string";
2364
+ if (!steering && (input.messages.length || input.resume?.length)) liveRun.current = input.runId;
2348
2365
  optsRef.current.onSend?.(input);
2349
2366
  let ended = false;
2350
2367
  try {
@@ -2368,6 +2385,10 @@ function usePilot(transport, opts = {}) {
2368
2385
  commit({ ...ref.current, notice: err instanceof Error ? err.message : null, effects: [] });
2369
2386
  return runNow({ messages: [] }, true);
2370
2387
  }
2388
+ if (steering && !reopening) {
2389
+ commit({ ...ref.current, notice: err instanceof Error && err.message ? err.message : null, effects: [] });
2390
+ return runNow({ messages: [] }, true);
2391
+ }
2371
2392
  commit(sendFailed(ref.current, err instanceof Error && err.message ? err.message : "No se pudo hablar con el agente."));
2372
2393
  return;
2373
2394
  }
@@ -2386,9 +2407,17 @@ function usePilot(transport, opts = {}) {
2386
2407
  ...ref.current.permissions.filter((p) => !answers.current.has(p.id)).map((p) => ({ interruptId: p.id, status: "cancelled" })),
2387
2408
  ...[...answers.current.entries()].map(([interruptId, a]) => ({ interruptId, ...a }))
2388
2409
  ];
2410
+ const steerOn = () => optsRef.current.steer ?? ref.current.agentState.caps?.steer === true;
2389
2411
  const send = useCallback2(
2390
2412
  async (text, attachments = [], _extra = {}) => {
2391
- if (!transportRef.current || ref.current.busy) return;
2413
+ if (!transportRef.current) return;
2414
+ if (ref.current.busy) {
2415
+ const live = liveRun.current ?? ref.current.runId;
2416
+ if (!steerOn() || !live || !text.trim() || attachments.length) return;
2417
+ commit(beginSteer(ref.current, text));
2418
+ await runNow({ messages: [userMessage(text, [])], forwardedProps: { steer: live } });
2419
+ return;
2420
+ }
2392
2421
  const resume = pendingResume();
2393
2422
  answers.current.clear();
2394
2423
  let s = ref.current;
@@ -2450,7 +2479,27 @@ function usePilot(transport, opts = {}) {
2450
2479
  },
2451
2480
  [commit, sendAnswersIfComplete]
2452
2481
  );
2453
- return { state, threadId, send, stop, retry, respondPermission, recordDecision, expirePermission };
2482
+ const [loadingOlder, setLoadingOlder] = useState8(false);
2483
+ const loadingRef = useRef4(false);
2484
+ const loadOlder = useCallback2(async () => {
2485
+ const fn = optsRef.current.loadOlder;
2486
+ const h = ref.current.history;
2487
+ if (!fn || !h?.hasMoreBefore || loadingRef.current) return;
2488
+ loadingRef.current = true;
2489
+ setLoadingOlder(true);
2490
+ try {
2491
+ const page = await fn(h.before);
2492
+ if (ref.current.history !== h) return;
2493
+ commit(prependHistory(ref.current, page.messages ?? [], { hasMoreBefore: page.hasMoreBefore === true, ...page.before ? { before: page.before } : {} }));
2494
+ } catch (err) {
2495
+ commit({ ...ref.current, notice: err instanceof Error && err.message ? err.message : null, effects: [] });
2496
+ } finally {
2497
+ loadingRef.current = false;
2498
+ setLoadingOlder(false);
2499
+ }
2500
+ }, [commit]);
2501
+ const canSteer = Boolean(transport) && (opts.steer ?? state.agentState.caps?.steer === true);
2502
+ return { state, threadId, send, stop, retry, respondPermission, recordDecision, expirePermission, canSteer, loadOlder, loadingOlder };
2454
2503
  }
2455
2504
 
2456
2505
  // src/react/Piloto.tsx
@@ -2459,10 +2508,10 @@ var useIsoLayoutEffect = typeof window === "undefined" ? useEffect6 : useLayoutE
2459
2508
  var ANCHOR_GAP = 24;
2460
2509
  var Piloto = forwardRef3(function Piloto2(props, handle) {
2461
2510
  const { transport, pilot: external, locale = "es", theme = "light", features = {}, slots = {} } = props;
2462
- const own = usePilot(external ? null : transport, props);
2511
+ const own = usePilot(external ? null : transport, features.steer === void 0 ? props : { ...props, steer: features.steer });
2463
2512
  const pilot = external ?? own;
2464
2513
  const { state } = pilot;
2465
- const { turns, busy, error, notice, permissions } = state;
2514
+ const { turns, busy, error, notice, permissions, olderCount } = state;
2466
2515
  const strings = useMemo3(() => ({ ...locale === "en" ? STRINGS_EN : STRINGS_ES, ...props.strings }), [locale, props.strings]);
2467
2516
  const config = useMemo3(
2468
2517
  () => ({
@@ -2546,6 +2595,18 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2546
2595
  const touchY = useRef5(null);
2547
2596
  const [atBottom, setAtBottom] = useState9(true);
2548
2597
  const message = (n) => scroller.current?.querySelector(`[data-turn="${n}"]`) ?? null;
2598
+ const olderSeen = useRef5(olderCount);
2599
+ const heightBefore = useRef5(0);
2600
+ useIsoLayoutEffect(() => {
2601
+ const el = scroller.current;
2602
+ if (el && olderCount > olderSeen.current && heightBefore.current) el.scrollTop += el.scrollHeight - heightBefore.current;
2603
+ olderSeen.current = olderCount;
2604
+ heightBefore.current = 0;
2605
+ }, [olderCount]);
2606
+ const loadOlder = () => {
2607
+ heightBefore.current = scroller.current?.scrollHeight ?? 0;
2608
+ void pilot.loadOlder();
2609
+ };
2549
2610
  const placed = useRef5(false);
2550
2611
  useEffect6(() => {
2551
2612
  if (placed.current || turns.length === 0) return;
@@ -2567,10 +2628,10 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2567
2628
  useEffect6(() => {
2568
2629
  if (mine === mineSeen.current) return;
2569
2630
  mineSeen.current = mine;
2570
- if (mine) setAnchor(mineIndex);
2631
+ if (mine) setAnchor(mineIndex - olderCount);
2571
2632
  following.current = true;
2572
2633
  }, [mine, mineIndex]);
2573
- const gapAbove = (a) => anchor === 0 ? a.offsetTop : ANCHOR_GAP;
2634
+ const gapAbove = (a) => anchor === -olderCount ? a.offsetTop : ANCHOR_GAP;
2574
2635
  const measureSpacer = useCallback3(() => {
2575
2636
  const sc = scroller.current;
2576
2637
  const sp = spacer.current;
@@ -2632,7 +2693,7 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2632
2693
  onSubmit: send,
2633
2694
  onStop: pilot.stop,
2634
2695
  busy,
2635
- canSteer: false,
2696
+ canSteer: pilot.canSteer,
2636
2697
  placeholder,
2637
2698
  attachments: showFiles,
2638
2699
  upload: props.upload,
@@ -2646,8 +2707,12 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2646
2707
  draft.n
2647
2708
  )
2648
2709
  ] });
2710
+ const withPanel = (chat) => slots.sidePanel ? /* @__PURE__ */ jsxs8("div", { className: "flex min-h-0 flex-1 flex-row", children: [
2711
+ /* @__PURE__ */ jsx10("div", { className: "relative flex min-h-0 min-w-0 flex-1 flex-col", children: chat }),
2712
+ slots.sidePanel(state.agentState)
2713
+ ] }) : chat;
2649
2714
  const permissionCards = cards.map((p) => /* @__PURE__ */ jsx10(ConfirmCard, { request: p, onRespond: respond, onSettled: settle, onExpire: pilot.expirePermission }, p.id));
2650
- 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: empty ? (
2715
+ 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 ? (
2651
2716
  // Un hilo vacío enseña lo mismo que la portada: la cara, el saludo, la caja y por dónde
2652
2717
  // empezar. Si no cabe (panel bajo), se desplaza: `safe center` nunca empuja nada fuera.
2653
2718
  /* @__PURE__ */ jsxs8("div", { className: "gs-scroll flex min-h-0 flex-1 flex-col items-center justify-center overflow-y-auto px-4 pb-[10vh] [justify-content:safe_center]", children: [
@@ -2688,10 +2753,20 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2688
2753
  className: "gs-scroll relative min-h-0 flex-1 overflow-y-auto overflow-x-hidden",
2689
2754
  children: [
2690
2755
  /* @__PURE__ */ jsxs8("div", { ref: column, className: "mx-auto flex w-full max-w-[784px] flex-col gap-6 px-4 py-8 sm:px-6", children: [
2756
+ state.history?.hasMoreBefore && (external || props.loadOlder) && /* @__PURE__ */ jsx10(
2757
+ "button",
2758
+ {
2759
+ type: "button",
2760
+ onClick: loadOlder,
2761
+ disabled: pilot.loadingOlder,
2762
+ className: "self-center rounded-full border border-border-primary px-3 py-1 text-sm text-text-secondary transition hover:text-text-primary disabled:opacity-60",
2763
+ children: pilot.loadingOlder ? t.loadingOlder : t.loadOlder
2764
+ }
2765
+ ),
2691
2766
  turns.map((turn, i) => {
2692
2767
  const isLast = i === turns.length - 1;
2693
2768
  const live = busy && isLast;
2694
- return /* @__PURE__ */ jsx10("div", { "data-turn": i, children: turn.role === "user" ? /* @__PURE__ */ jsx10(UserBubble, { turn }) : /* @__PURE__ */ jsx10(
2769
+ return /* @__PURE__ */ jsx10("div", { "data-turn": i - olderCount, children: turn.role === "user" ? /* @__PURE__ */ jsx10(UserBubble, { turn }) : /* @__PURE__ */ jsx10(
2695
2770
  AgentBubble,
2696
2771
  {
2697
2772
  turn,
@@ -2703,12 +2778,12 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2703
2778
  showDuration: features.duration !== false,
2704
2779
  renderDeliveries: slots.renderDeliveries
2705
2780
  }
2706
- ) }, i);
2781
+ ) }, i - olderCount);
2707
2782
  }),
2708
2783
  permissionCards,
2709
2784
  busy && !toolRunning && !cards.length && /* @__PURE__ */ jsxs8("div", { className: "flex flex-col gap-1.5", children: [
2710
2785
  /* @__PURE__ */ jsx10(TypingDots, {}),
2711
- /* @__PURE__ */ jsx10(WaitLabel, { since: state.turnStartedAt, showDuration: features.duration !== false })
2786
+ /* @__PURE__ */ jsx10(WaitLabel, { since: state.turnStartedAt, step: state.step, showDuration: features.duration !== false })
2712
2787
  ] }),
2713
2788
  notice && /* @__PURE__ */ jsx10("p", { className: "text-sm text-text-tertiary", children: notice }),
2714
2789
  error && /* @__PURE__ */ jsx10("p", { className: "text-sm text-text-danger", children: error })
@@ -2731,7 +2806,7 @@ var Piloto = forwardRef3(function Piloto2(props, handle) {
2731
2806
  slots.aboveComposer,
2732
2807
  composer
2733
2808
  ] })
2734
- ] }) }) });
2809
+ ] })) }) });
2735
2810
  });
2736
2811
  export {
2737
2812
  AgentBubble,
@@ -2765,9 +2840,11 @@ export {
2765
2840
  UsageStats,
2766
2841
  UserBubble,
2767
2842
  WaitLabel,
2843
+ applyJsonPatch,
2768
2844
  applyReferences,
2769
2845
  argsDetail,
2770
2846
  beginSend,
2847
+ beginSteer,
2771
2848
  cleanReplyOption,
2772
2849
  cleanUserText,
2773
2850
  createAgUiTransport,
@@ -2784,6 +2861,7 @@ export {
2784
2861
  messagesToTurns,
2785
2862
  mimeOf,
2786
2863
  narrationToMarkdown,
2864
+ prependHistory,
2787
2865
  readSse,
2788
2866
  reduceAgUiEvent,
2789
2867
  renderInline,
@@ -54,6 +54,10 @@ export interface PilotStrings {
54
54
  linkOpen: string;
55
55
  close: string;
56
56
  goToLatest: string;
57
+ /** La frase de espera de cada paso con nombre (`STEP_STARTED.stepName`). Los que no estén aquí no se pintan. */
58
+ steps: Record<string, string>;
59
+ loadOlder: string;
60
+ loadingOlder: string;
57
61
  confirmTitle: string;
58
62
  approve: string;
59
63
  reject: string;
@@ -1,5 +1,5 @@
1
- import type { BaseEvent, RunAgentInput } from "@ag-ui/core";
2
- import type { Attachment, Decision, PermissionRequest, PilotState, Reference, Turn } from "../core/types";
1
+ import type { BaseEvent, Message, RunAgentInput } from "@ag-ui/core";
2
+ import type { Attachment, Decision, HistoryPage, PermissionRequest, PilotState, Reference, Turn } from "../core/types";
3
3
  import { type PilotTransport } from "../transport";
4
4
  export interface TurnDoneInfo {
5
5
  /** El texto final de la respuesta. */
@@ -31,6 +31,20 @@ export interface UsePilotOptions {
31
31
  onEvent?: (event: BaseEvent) => void;
32
32
  /** Cada run que sale (analítica). */
33
33
  onSend?: (input: RunAgentInput) => void;
34
+ /** El estado del agente cambió (`STATE_SNAPSHOT` / `STATE_DELTA` ya aplicado). */
35
+ onState?: (state: Record<string, unknown>) => void;
36
+ /**
37
+ * Steer: mandar con el run vivo (docs/protocol.md §6). Default: lo que diga el agente en
38
+ * `state.caps.steer`. Sin steer, mandar mientras corre no hace nada.
39
+ */
40
+ steer?: boolean;
41
+ /**
42
+ * Trae los mensajes anteriores a `before` (el cursor de `CUSTOM ghosty.history`), p. ej. del REST de
43
+ * lectura de la app. Las páginas empiezan en un mensaje del usuario.
44
+ */
45
+ loadOlder?: (before: string | undefined) => Promise<{
46
+ messages: Message[];
47
+ } & HistoryPage>;
34
48
  }
35
49
  export interface SendOptions {
36
50
  references?: Reference[];
@@ -49,6 +63,11 @@ export interface Pilot {
49
63
  recordDecision: (id: string, status: Decision["status"], asked?: PermissionRequest) => void;
50
64
  /** Una tarjeta venció sin contestarse: se anota y su interrupt viaja como `cancelled` en el siguiente run. */
51
65
  expirePermission: (id: string) => void;
66
+ /** Se puede mandar con el run vivo (steer). */
67
+ canSteer: boolean;
68
+ /** Antepone la página anterior del hilo (si hay `loadOlder` y `state.history.hasMoreBefore`). */
69
+ loadOlder: () => Promise<void>;
70
+ loadingOlder: boolean;
52
71
  }
53
72
  /** El contexto de la app como `context` de AG-UI: `[{description, value}]`, `value` siempre texto. */
54
73
  export declare function toAgUiContext(ctx: unknown): RunAgentInput["context"];
package/dist/styles.css CHANGED
@@ -511,6 +511,9 @@
511
511
  .piloto .flex-col {
512
512
  flex-direction: column;
513
513
  }
514
+ .piloto .flex-row {
515
+ flex-direction: row;
516
+ }
514
517
  .piloto .flex-wrap {
515
518
  flex-wrap: wrap;
516
519
  }
@@ -588,6 +591,9 @@
588
591
  .piloto :where(.divide-border > :not(:last-child)) {
589
592
  border-color: var(--color-border);
590
593
  }
594
+ .piloto .self-center {
595
+ align-self: center;
596
+ }
591
597
  .piloto .truncate {
592
598
  overflow: hidden;
593
599
  text-overflow: ellipsis;
@@ -1404,6 +1410,9 @@
1404
1410
  .piloto .disabled\:opacity-50:disabled {
1405
1411
  opacity: 50%;
1406
1412
  }
1413
+ .piloto .disabled\:opacity-60:disabled {
1414
+ opacity: 60%;
1415
+ }
1407
1416
  @supports (backdrop-filter: var(--tw)) {
1408
1417
  .piloto .supports-\[backdrop-filter\]\:bg-background\/70 {
1409
1418
  background-color: var(--color-background);
@@ -2,7 +2,7 @@ import {
2
2
  PilotHttpError,
3
3
  createAgUiTransport,
4
4
  readSse
5
- } from "../chunks/chunk-DXFWACUL.js";
5
+ } from "../chunks/chunk-TMEHQZWL.js";
6
6
  export {
7
7
  PilotHttpError,
8
8
  createAgUiTransport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghostykit",
3
- "version": "0.4.0",
3
+ "version": "0.5.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",