ghostykit 0.6.3 → 0.7.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
@@ -97,34 +97,109 @@ const ref = useRef<PilotoHandle>(null);
97
97
  draft={{ text: "Why did the monitor fail?", key: row.id }} // prefilled + focused, not sent
98
98
  threadId={conversationId} // reopen a stored thread
99
99
  context={() => ({ screen: "crons", selected })} // AG-UI `context`, every run
100
- toolVerbs={{ run_cron: { one: "Ran a cron", many: "Ran %n crons", running: "Running the cron…" } }}
101
- upload={async (file) => ({ url: await myUpload(file) })}
102
- transcribe={async (audio) => myWhisper(audio)}
103
100
  onTurnDone={({ text }) => refreshMyData()}
104
- slots={{ renderDeliveries: (turn) => <MyFiles turn={turn} />, composerExtras: <MyButton /> }}
101
+ plugins={PLUGINS} // everything else: see Customize
105
102
  />;
106
103
 
107
104
  ref.current?.addReference({ id: "12", label: "#12 Rosa" }); // "@" a row into the message
108
105
  ref.current?.send("Why did this fail?"); // ask on the user's behalf
109
106
  ```
110
107
 
111
- Other options: `strings`, `toolIcons`, `formatUserText`, `markdown`, `features`
112
- (`attachments`, `retry`, `copy`, `usage`, `replyOptions`, `duration`, `steer`), `references` /
113
- `onReferencesChange`, `voiceNoteText`, `placeholder`, `avatar`, `initialTurns`, `loadOlder` (earlier
114
- pages of a long thread), `forwardedProps` (extra keys for every run, e.g. `{permissions: "ask"}`), the
115
- slots `sidePanel` and `aboveComposer` (both may take the `pilot`), and the callbacks `onTurnStart`,
116
- `onTitle`, `onState`, `onEvent` (every raw AG-UI event), `onSend`. The agent's shared state
117
- (`STATE_SNAPSHOT` + `STATE_DELTA`) is `pilot.state.agentState`, and each turn's `CUSTOM` events are
118
- `turn.custom` (what `renderDeliveries` draws).
108
+ Other props: `features` (`attachments`, `retry`, `copy`, `usage`, `replyOptions`, `duration`,
109
+ `steer`), `references` / `onReferencesChange`, `placeholder`, `avatar`, `initialTurns`, `loadOlder`
110
+ (earlier pages of a long thread), `forwardedProps` (extra keys for every run, e.g.
111
+ `{permissions: "ask"}`), and the callbacks `onTurnStart`, `onTitle`, `onState`, `onEvent` (every raw
112
+ AG-UI event), `onSend`. The agent's shared state (`STATE_SNAPSHOT` + `STATE_DELTA`) is
113
+ `pilot.state.agentState`, and each turn's `CUSTOM` events are `turn.custom`.
119
114
 
120
115
  Subagents (`state.subagents.tasks`) are `pilot.subagents`; `pilot.stopSubagent(id)` stops one, and
121
- `<Subagents pilot={pilot} />` is a ready-made bar ("2 agents working", one row each, *Stop*):
122
- `slots={{ aboveComposer: (pilot) => <Subagents pilot={pilot} /> }}`. A message the server can't take
123
- while a run is live (`409 run_active`) waits in `pilot.state.queued` and is sent when that run ends.
124
- `pilot.refresh()` asks for the thread again (when your own event bus says something changed). To read the state outside the chat, call `usePilot(transport)` yourself
125
- and pass the result as `pilot`. Every piece is
126
- exported (`AgentBubble`, `AgentSteps`, `Composer`, `ConfirmCard`, `Markdown`, …) if you want to build
127
- your own layout.
116
+ `<Subagents pilot={pilot} />` is a ready-made bar ("2 agents working", one row each, *Stop*). A
117
+ message the server can't take while a run is live (`409 run_active`) waits in `pilot.state.queued`
118
+ and is sent when that run ends. `pilot.refresh()` asks for the thread again (when your own event bus
119
+ says something changed). To read the state outside the chat, call `usePilot(transport)` yourself and
120
+ pass the result as `pilot`. Every piece is exported (`AgentBubble`, `AgentSteps`, `Composer`,
121
+ `ConfirmCard`, `Markdown`, …) if you want to build your own layout.
122
+
123
+ ## Customize
124
+
125
+ There is **one** way to customize `<Piloto>`: **plugins**. A plugin is an object with a `name` and
126
+ named extension points. Everything a plugin draws is a React **component**: it is mounted as
127
+ `<View ctx={…} />`, so it can use hooks. Branding is the skin plus `--piloto-*` tokens.
128
+
129
+ ```tsx
130
+ import { Piloto, definePlugin } from "ghostykit";
131
+ import { livePage, modelPicker, pilotDevtools } from "ghostykit/plugins";
132
+
133
+ const crm = definePlugin({
134
+ name: "crm",
135
+ requires: { events: ["crm.card"] }, // what it handles, visible at a glance
136
+ tools: { verbs: { find_lead: { one: "Found the lead", many: "Found %n leads", running: "Looking…" } } },
137
+ message: { custom: { "crm.card": ({ entry, ctx }) => <LeadCard lead={entry.value} onOpen={() => ctx.send("Open it")} /> } },
138
+ composer: { actions: ({ ctx }) => <PointButton onPick={(row) => ctx.addReference(row)} /> },
139
+ });
140
+
141
+ // Create them ONCE (module scope or useMemo): some plugins keep state.
142
+ const PLUGINS = [crm, modelPicker({ onChange: (model) => api.setModel(model) }), livePage(), pilotDevtools()];
143
+
144
+ <Piloto transport={transport} skin="ghosty" plugins={PLUGINS} />;
145
+ ```
146
+
147
+ | Point | What it does | Example |
148
+ |---|---|---|
149
+ | `name` | Unique. Marks everything it draws with `data-plugin="<name>"`. | `"crm"` |
150
+ | `styles` | Whose styles its views use. `"host"` (default): your app's; ghostykit's utilities stay out (`data-piloto-isolate`), so they never override your classes. `"ghostykit"`: for views built from ghostykit components (`<Subagents>`, `ModelPicker`). | `"ghostykit"` |
151
+ | `requires` | `{events, state}` it handles. Derived from `events`/`state`/`message.custom` if missing; in development a mismatch is a warning. | `{ events: ["ghosty.page"] }` |
152
+ | `strings` | Texts over the locale's. | `{ stop: "Para" }` |
153
+ | `markdown.transform` | Rewrites the answer's markdown, with the turn at hand (chained). | `(md, turn) => md.replace(/#(\d+)/g, "[#$1](#folio-$1)")` |
154
+ | `markdown.onLinkClick` | A click on a link; `true` if handled (asked in order). | open in-app |
155
+ | `markdown.blocks` | Fenced blocks by name (```` ```eb-file ````) taken out of the text and drawn as cards under the answer; one still being written comes with `complete: false`. | `{ "eb-file": FileCard }` |
156
+ | `vars`, `className` | Branding on the root: `--piloto-*` tokens and classes. | `{ "--piloto-radius-bubble": "6px" }` |
157
+ | `tools.verbs` | How each tool is named in the steps. | `{ run_cron: { one, many, running } }` |
158
+ | `tools.icons` | Icon by tool name or `kind`. | `{ run_cron: Clock }` |
159
+ | `tools.render` | A tool's own view, under the steps, by tool name or `kind`. | `{ create_image: ImagePreview }` |
160
+ | `events` | Handlers by AG-UI type, by `CUSTOM` name, or `*`. | `{ "ghosty.ui": (e, ctx) => board.open(e.value) }` |
161
+ | `state` | Called when that key of `agentState` changes. | `{ model: (v, ctx) => … }` |
162
+ | `message.custom` | Draws a `turn.custom` entry by its name (deliveries, CRM cards). | `{ "ghosty.delivery": DeliveryCard }` |
163
+ | `message.assistantBefore` | Before each answer's text, under its steps. | live page progress, "Creating image…" |
164
+ | `message.user` / `message.assistant` | Extras under each user message / answer. | `({ turn, live }) => …` |
165
+ | `message.formatUserText` | How the user's text is shown (chained). | strip app plumbing |
166
+ | `composer.actions` | Buttons in the composer bar, between the image button and the clip. | "My files", "Point" |
167
+ | `composer.actionsEnd` | Buttons after the clip. | model picker |
168
+ | `composer.above` | Above the message box. | `<Subagents pilot={ctx.pilot} />` |
169
+ | `composer.below` | Under the message box. | suggestion chips |
170
+ | `composer.attachments` | `upload`, `transcribe`, `voiceNoteText`. | `{ upload: async (f) => ({ url }) }` |
171
+ | `header` | Top right, over the thread. | share button |
172
+ | `sidePanel` | Next to the chat. | live page, subagents |
173
+ | `aboveThread` | Above the messages. | a banner |
174
+ | `empty` | The empty thread (instead of the greeting). | onboarding |
175
+ | `waiting` | While the agent hasn't answered (instead of the waiting phrase). Gets `step`, `since`. | "Creating your image…" |
176
+ | `wrap` | A provider around the whole chat (`{ctx, children}`); the first plugin is outermost. | artifacts panel |
177
+ | `onTurnStart` / `onTurnDone` | This client started / finished a turn. | open the last delivery |
178
+
179
+ Every view gets `ctx`: `{pilot, agentState, strings, send, addReference, addAttachments, setDraft, focus, plugins}`.
180
+ `addAttachments` takes `File`s (they go through `upload`) or ready `Attachment`s (e.g. from "My files"),
181
+ and they can be sent without text. The `ref` of `<Piloto>` is the same handle (without `plugins`,
182
+ `strings`, `agentState`), so the app never needs a separate `usePilot`: `ref.current.addReference(…)`,
183
+ `ref.current.pilot.reconnect()` (when your own event bus says the thread changed),
184
+ `ref.current.pilot.stop()`. `initialPrompt` sends a message as soon as the thread is open (a question
185
+ in the URL).
186
+
187
+ **Order and precedence**, in the order of the `plugins` list:
188
+
189
+ - **Maps** (`strings`, `tools.verbs`, `tools.icons`, `vars`) are merged, and later plugins win. `markdown` and `formatUserText` are chained.
190
+ - **`events` and `state`** call every handler, in order.
191
+ - **Keyed or single views** (`tools.render`, `message.custom`, `markdown.blocks`, `empty`, `waiting`, `composer.attachments`): the first plugin that declares one wins. A clash is a development warning.
192
+ - **Lists** (`header`, `sidePanel`, `aboveThread`, `composer.actions`/`actionsEnd`/`above`/`below`, `message.user`/`assistant`/`assistantBefore`, `wrap`, `onTurnStart`/`onTurnDone`) all apply, in order.
193
+
194
+ In development, ghostykit also warns once about a `CUSTOM` event that nothing handles.
195
+
196
+ **Ready-made plugins** (`ghostykit/plugins`), usable as they are and as templates:
197
+
198
+ | Plugin | What it does |
199
+ |---|---|
200
+ | `modelPicker({ onChange, stateKey? })` | The model picker in the composer bar, from `agentState.model = {current, options}`. Switching is your API (`onChange`). |
201
+ | `livePage({ width? })` | A side panel that follows `CUSTOM ghosty.page` (plan, sections, ready, error) with the link at the end. |
202
+ | `pilotDevtools({ events? })` | Prints the mounted plugins, their points and what they handle; with `events: true`, every event and who handles it. |
128
203
 
129
204
  ### Skins and theming
130
205
 
@@ -193,7 +193,7 @@ function toolLabel(t, v) {
193
193
  if (str(x.title)) patch.label = str(x.title);
194
194
  if (str(x.kind)) patch.kind = str(x.kind);
195
195
  if (str(x.path)) patch.path = str(x.path);
196
- if (str(x.detail)) patch.detail = str(x.detail);
196
+ if (str(x.detail)) Object.assign(patch, { detail: str(x.detail), detailFromServer: true });
197
197
  t.tools = (t.tools ?? []).map((y) => y.id === x.toolCallId ? { ...y, ...patch } : y);
198
198
  }
199
199
  function applyStoredCustom(t, m) {
@@ -259,7 +259,8 @@ function applySnapshot(s, messages) {
259
259
  const tools = t.tools?.map((x) => {
260
260
  const prev = o?.role === t.role ? o.tools?.find((y) => y.id === x.id) : void 0;
261
261
  const status = x.status === "pending" && (prev?.status ?? (live ? "in_progress" : void 0));
262
- return { ...prev, ...x, ...status ? { status } : {} };
262
+ const keepDetail = prev?.detailFromServer && !x.detailFromServer ? { detail: prev.detail, detailFromServer: true } : {};
263
+ return { ...prev, ...x, ...status ? { status } : {}, ...keepDetail };
263
264
  });
264
265
  const base = tools ? { ...t, tools } : t;
265
266
  if (!o || o.role !== t.role) return base;
@@ -325,6 +326,7 @@ function reduceAgUiEvent(prev, e, now = Date.now()) {
325
326
  }
326
327
  case "TOOL_CALL_END": {
327
328
  const known = s.turns[s.turns.length - 1]?.tools?.find((x) => x.id === e.toolCallId);
329
+ if (known?.detailFromServer) return s;
328
330
  return upsertTool(s, e.toolCallId, { detail: argsDetail(known?.args) });
329
331
  }
330
332
  case "TOOL_CALL_RESULT": {
@@ -369,7 +371,7 @@ function reduceAgUiEvent(prev, e, now = Date.now()) {
369
371
  if (str(v.title)) patch.label = str(v.title);
370
372
  if (str(v.kind)) patch.kind = str(v.kind);
371
373
  if (str(v.path)) patch.path = str(v.path);
372
- if (str(v.detail)) patch.detail = str(v.detail);
374
+ if (str(v.detail)) Object.assign(patch, { detail: str(v.detail), detailFromServer: true });
373
375
  return upsertTool(s, v.toolCallId, patch);
374
376
  }
375
377
  if (e.name === "ghosty.replace") {
@@ -538,7 +540,7 @@ function isNarration(chunk) {
538
540
  const t = chunk.trim();
539
541
  return t.length > 0 && t.length <= 300 && !/\n\s*\n/.test(t) && !t.includes("```");
540
542
  }
541
- function splitNarration(text, cuts) {
543
+ function splitNarration(text, cuts, closed = false) {
542
544
  if (cuts?.length) {
543
545
  const valid = cuts.filter((c) => c > 0 && c <= text.length);
544
546
  if (valid.length) {
@@ -550,7 +552,9 @@ function splitNarration(text, cuts) {
550
552
  from = c;
551
553
  }
552
554
  if (chunks.every(isNarration)) {
553
- return { steps: chunks.map((t) => t.replace(/\s*\n\s*/g, " ")), rest: text.slice(from).replace(/^\s+/, "") };
555
+ const rest = text.slice(from).replace(/^\s+/, "");
556
+ if (closed && !rest.trim()) return { steps: chunks.slice(0, -1).map((t) => t.replace(/\s*\n\s*/g, " ")), rest: chunks[chunks.length - 1] };
557
+ return { steps: chunks.map((t) => t.replace(/\s*\n\s*/g, " ")), rest };
554
558
  }
555
559
  }
556
560
  return { steps: [], rest: text };
@@ -574,6 +578,30 @@ function narrationToMarkdown(chunks, answer) {
574
578
 
575
579
  ${answer.trim()}` : list;
576
580
  }
581
+ function extractBlocks(text, langs, cuts) {
582
+ if (!langs.length || !text.includes("```")) return { text, cuts, blocks: [] };
583
+ const want = new Set(langs);
584
+ const blocks = [];
585
+ const removed = [];
586
+ const re = /(^|\n)```([\w.-]+)[^\n]*\n([\s\S]*?)(\n```[ \t]*(?=\n|$)|$)/g;
587
+ for (let m = re.exec(text); m; m = re.exec(text)) {
588
+ if (!want.has(m[2])) continue;
589
+ const start = m.index + m[1].length;
590
+ const end = m.index + m[0].length;
591
+ blocks.push({ lang: m[2], body: m[3], complete: Boolean(m[4]) });
592
+ removed.push([start, end]);
593
+ }
594
+ if (!removed.length) return { text, cuts, blocks };
595
+ let out = "";
596
+ let from = 0;
597
+ for (const [a, b] of removed) {
598
+ out += text.slice(from, a);
599
+ from = b;
600
+ }
601
+ out += text.slice(from);
602
+ const shift = (c) => c - removed.reduce((n, [a, b]) => n + (c >= b ? b - a : c > a ? c - a : 0), 0);
603
+ return { text: out, ...cuts ? { cuts: cuts.map(shift) } : {}, blocks };
604
+ }
577
605
 
578
606
  // src/core/options.ts
579
607
  var BULLET = /^\s*(?:[-*•+]|(\d{1,2})[.)])\s+(.+?)\s*$/;
@@ -604,140 +632,6 @@ function cleanReplyOption(t) {
604
632
  return s.charAt(0).toLocaleUpperCase() + s.slice(1);
605
633
  }
606
634
 
607
- // src/core/steps.ts
608
- var isRunning = (t) => t.status !== "completed" && t.status !== "failed" && t.status !== "cancelled";
609
- function toolNames(t) {
610
- const title = t.title ?? "";
611
- if (!title) return [];
612
- const out = [title];
613
- const double = /^mcp__.+?__(.+)$/i.exec(title);
614
- if (double) out.push(double[1]);
615
- else if (/^mcp_/i.test(title)) {
616
- const parts = title.slice(4).split("_");
617
- for (let i = 0; i < parts.length; i++) out.push(parts.slice(i).join("_"));
618
- }
619
- return [...new Set(out)];
620
- }
621
- function toolName(t) {
622
- const names = toolNames(t);
623
- return /^mcp__/i.test(names[0] ?? "") ? names[1] ?? names[0] : names[0] ?? null;
624
- }
625
- function namedVerb(t, verbs) {
626
- for (const name of toolNames(t)) if (verbs[name]) return { key: name, verb: verbs[name] };
627
- return null;
628
- }
629
- function stepLabel(t, verbs, fallback = "Paso") {
630
- if (t.label) return t.label;
631
- const named = namedVerb(t, verbs);
632
- if (named) return isRunning(t) && named.verb.running ? named.verb.running : named.verb.one;
633
- return t.title ?? t.kind ?? fallback;
634
- }
635
- function summarizeTools(tools, verbs) {
636
- const groups = /* @__PURE__ */ new Map();
637
- for (const t of tools) {
638
- const named = namedVerb(t, verbs);
639
- const key = named?.key ?? t.kind ?? "other";
640
- const g = groups.get(key) ?? { n: 0, verb: named?.verb ?? verbs[key] };
641
- g.n++;
642
- groups.set(key, g);
643
- }
644
- const parts = [];
645
- for (const [key, { n, verb }] of groups) {
646
- if (verb) parts.push(n === 1 ? verb.one : verb.many.replace("%n", String(n)));
647
- else parts.push(`${n} \xD7 ${key.replace(/[_-]/g, " ")}`);
648
- }
649
- return parts.join(" \xB7 ");
650
- }
651
- function groupSteps(tools, label) {
652
- const groups = /* @__PURE__ */ new Map();
653
- for (const t of tools) {
654
- const k = `${label(t)}\0${t.detail ?? ""}`;
655
- const g = groups.get(k);
656
- if (!g) {
657
- groups.set(k, { tool: t, times: 1 });
658
- continue;
659
- }
660
- g.times++;
661
- const wins = isRunning(t) || t.status === "failed" || !(isRunning(g.tool) || g.tool.status === "failed");
662
- if (wins) g.tool = { ...t, id: g.tool.id };
663
- }
664
- return [...groups.values()];
665
- }
666
- var TOOL_VERBS_ES = {
667
- read: { one: "Le\xED un archivo", many: "Le\xED %n archivos" },
668
- read_file: { one: "Le\xED un archivo", many: "Le\xED %n archivos" },
669
- write: { one: "Escrib\xED un archivo", many: "Escrib\xED %n archivos" },
670
- write_file: { one: "Escrib\xED un archivo", many: "Escrib\xED %n archivos" },
671
- edit: { one: "Edit\xE9 un archivo", many: "Edit\xE9 %n archivos" },
672
- edit_file: { one: "Edit\xE9 un archivo", many: "Edit\xE9 %n archivos" },
673
- bash: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
674
- shell: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
675
- terminal: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
676
- execute: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
677
- grep: { one: "Busqu\xE9 en el c\xF3digo", many: "Busqu\xE9 en el c\xF3digo" },
678
- glob: { one: "Busqu\xE9 archivos", many: "Busqu\xE9 archivos" },
679
- search: { one: "Busqu\xE9", many: "Busqu\xE9 %n veces" },
680
- tool_search: { one: "Busqu\xE9 una herramienta", many: "Busqu\xE9 %n herramientas" },
681
- web_search: { one: "Busqu\xE9 en la web", many: "Busqu\xE9 en la web" },
682
- web_fetch: { one: "Abr\xED una p\xE1gina", many: "Abr\xED %n p\xE1ginas" },
683
- fetch: { one: "Abr\xED una p\xE1gina", many: "Abr\xED %n p\xE1ginas" },
684
- site_publish: { one: "Publiqu\xE9 una p\xE1gina", many: "Publiqu\xE9 %n p\xE1ginas" },
685
- pdf: { one: "Gener\xE9 un PDF", many: "Gener\xE9 %n PDF" },
686
- connector: { one: "Us\xE9 tu sistema", many: "Us\xE9 tu sistema %n veces" },
687
- mcp: { one: "Us\xE9 una herramienta", many: "Us\xE9 %n herramientas" },
688
- browser: { one: "Us\xE9 el navegador", many: "Us\xE9 el navegador" },
689
- todo: { one: "Anot\xE9 el plan", many: "Anot\xE9 el plan" },
690
- delete: { one: "Borr\xE9 un archivo", many: "Borr\xE9 %n archivos" },
691
- move: { one: "Mov\xED un archivo", many: "Mov\xED %n archivos" },
692
- think: { one: "Lo pens\xE9", many: "Lo pens\xE9" },
693
- image: { one: "Cre\xE9 una imagen", many: "Cre\xE9 %n im\xE1genes" },
694
- delegate: { one: "Delegu\xE9 una tarea", many: "Delegu\xE9 %n tareas" },
695
- collect: { one: "Recog\xED un resultado", many: "Recog\xED %n resultados" },
696
- status: { one: "Pregunt\xE9 c\xF3mo iba", many: "Pregunt\xE9 %n veces c\xF3mo iba" },
697
- stop: { one: "Detuve una tarea", many: "Detuve %n tareas" },
698
- guide: { one: "Le\xED una gu\xEDa", many: "Le\xED %n gu\xEDas" },
699
- deliver: { one: "Entregu\xE9 un archivo", many: "Entregu\xE9 %n archivos" },
700
- sheet: { one: "Consult\xE9 una hoja", many: "Consult\xE9 %n hojas" },
701
- view: { one: "Arm\xE9 una vista", many: "Arm\xE9 %n vistas" }
702
- };
703
- var TOOL_VERBS_EN = {
704
- read: { one: "Read a file", many: "Read %n files" },
705
- read_file: { one: "Read a file", many: "Read %n files" },
706
- write: { one: "Wrote a file", many: "Wrote %n files" },
707
- write_file: { one: "Wrote a file", many: "Wrote %n files" },
708
- edit: { one: "Edited a file", many: "Edited %n files" },
709
- edit_file: { one: "Edited a file", many: "Edited %n files" },
710
- bash: { one: "Ran a command", many: "Ran %n commands" },
711
- shell: { one: "Ran a command", many: "Ran %n commands" },
712
- terminal: { one: "Ran a command", many: "Ran %n commands" },
713
- execute: { one: "Ran a command", many: "Ran %n commands" },
714
- grep: { one: "Searched the code", many: "Searched the code" },
715
- glob: { one: "Searched for files", many: "Searched for files" },
716
- search: { one: "Searched", many: "Searched %n times" },
717
- tool_search: { one: "Looked for a tool", many: "Looked for %n tools" },
718
- web_search: { one: "Searched the web", many: "Searched the web" },
719
- web_fetch: { one: "Opened a page", many: "Opened %n pages" },
720
- fetch: { one: "Opened a page", many: "Opened %n pages" },
721
- site_publish: { one: "Published a page", many: "Published %n pages" },
722
- pdf: { one: "Generated a PDF", many: "Generated %n PDFs" },
723
- connector: { one: "Used your system", many: "Used your system %n times" },
724
- mcp: { one: "Used a tool", many: "Used %n tools" },
725
- browser: { one: "Used the browser", many: "Used the browser" },
726
- todo: { one: "Wrote down the plan", many: "Wrote down the plan" },
727
- delete: { one: "Deleted a file", many: "Deleted %n files" },
728
- move: { one: "Moved a file", many: "Moved %n files" },
729
- think: { one: "Thought it through", many: "Thought it through" },
730
- image: { one: "Created an image", many: "Created %n images" },
731
- delegate: { one: "Delegated a task", many: "Delegated %n tasks" },
732
- collect: { one: "Collected a result", many: "Collected %n results" },
733
- status: { one: "Checked how it was going", many: "Checked %n times how it was going" },
734
- stop: { one: "Stopped a task", many: "Stopped %n tasks" },
735
- guide: { one: "Read a guide", many: "Read %n guides" },
736
- deliver: { one: "Delivered a file", many: "Delivered %n files" },
737
- sheet: { one: "Checked a sheet", many: "Checked %n sheets" },
738
- view: { one: "Built a view", many: "Built %n views" }
739
- };
740
-
741
635
  export {
742
636
  applyJsonPatch,
743
637
  createPilotState,
@@ -761,14 +655,7 @@ export {
761
655
  isNarration,
762
656
  splitNarration,
763
657
  narrationToMarkdown,
658
+ extractBlocks,
764
659
  extractReplyOptions,
765
- cleanReplyOption,
766
- isRunning,
767
- toolNames,
768
- toolName,
769
- stepLabel,
770
- summarizeTools,
771
- groupSteps,
772
- TOOL_VERBS_ES,
773
- TOOL_VERBS_EN
660
+ cleanReplyOption
774
661
  };
@@ -0,0 +1,144 @@
1
+ // src/core/steps.ts
2
+ var isRunning = (t) => t.status !== "completed" && t.status !== "failed" && t.status !== "cancelled";
3
+ function toolNames(t) {
4
+ const title = t.title ?? "";
5
+ if (!title) return [];
6
+ const out = [title];
7
+ const double = /^mcp__.+?__(.+)$/i.exec(title);
8
+ if (double) out.push(double[1]);
9
+ else if (/^mcp_/i.test(title)) {
10
+ const parts = title.slice(4).split("_");
11
+ for (let i = 0; i < parts.length; i++) out.push(parts.slice(i).join("_"));
12
+ }
13
+ return [...new Set(out)];
14
+ }
15
+ function toolName(t) {
16
+ const names = toolNames(t);
17
+ return /^mcp__/i.test(names[0] ?? "") ? names[1] ?? names[0] : names[0] ?? null;
18
+ }
19
+ function namedVerb(t, verbs) {
20
+ for (const name of toolNames(t)) if (verbs[name]) return { key: name, verb: verbs[name] };
21
+ return null;
22
+ }
23
+ function stepLabel(t, verbs, fallback = "Paso") {
24
+ if (t.label) return t.label;
25
+ const named = namedVerb(t, verbs);
26
+ if (named) return isRunning(t) && named.verb.running ? named.verb.running : named.verb.one;
27
+ return t.title ?? t.kind ?? fallback;
28
+ }
29
+ function summarizeTools(tools, verbs) {
30
+ const groups = /* @__PURE__ */ new Map();
31
+ for (const t of tools) {
32
+ const named = namedVerb(t, verbs);
33
+ const key = named?.key ?? t.kind ?? "other";
34
+ const g = groups.get(key) ?? { n: 0, verb: named?.verb ?? verbs[key] };
35
+ g.n++;
36
+ groups.set(key, g);
37
+ }
38
+ const parts = [];
39
+ for (const [key, { n, verb }] of groups) {
40
+ if (verb) parts.push(n === 1 ? verb.one : verb.many.replace("%n", String(n)));
41
+ else parts.push(`${n} \xD7 ${key.replace(/[_-]/g, " ")}`);
42
+ }
43
+ return parts.join(" \xB7 ");
44
+ }
45
+ function groupSteps(tools, label) {
46
+ const groups = /* @__PURE__ */ new Map();
47
+ for (const t of tools) {
48
+ const k = `${label(t)}\0${t.detail ?? ""}`;
49
+ const g = groups.get(k);
50
+ if (!g) {
51
+ groups.set(k, { tool: t, times: 1 });
52
+ continue;
53
+ }
54
+ g.times++;
55
+ const wins = isRunning(t) || t.status === "failed" || !(isRunning(g.tool) || g.tool.status === "failed");
56
+ if (wins) g.tool = { ...t, id: g.tool.id };
57
+ }
58
+ return [...groups.values()];
59
+ }
60
+ var TOOL_VERBS_ES = {
61
+ read: { one: "Le\xED un archivo", many: "Le\xED %n archivos" },
62
+ read_file: { one: "Le\xED un archivo", many: "Le\xED %n archivos" },
63
+ write: { one: "Escrib\xED un archivo", many: "Escrib\xED %n archivos" },
64
+ write_file: { one: "Escrib\xED un archivo", many: "Escrib\xED %n archivos" },
65
+ edit: { one: "Edit\xE9 un archivo", many: "Edit\xE9 %n archivos" },
66
+ edit_file: { one: "Edit\xE9 un archivo", many: "Edit\xE9 %n archivos" },
67
+ bash: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
68
+ shell: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
69
+ terminal: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
70
+ execute: { one: "Corr\xED un comando", many: "Corr\xED %n comandos" },
71
+ grep: { one: "Busqu\xE9 en el c\xF3digo", many: "Busqu\xE9 en el c\xF3digo" },
72
+ glob: { one: "Busqu\xE9 archivos", many: "Busqu\xE9 archivos" },
73
+ search: { one: "Busqu\xE9", many: "Busqu\xE9 %n veces" },
74
+ tool_search: { one: "Busqu\xE9 una herramienta", many: "Busqu\xE9 %n herramientas" },
75
+ web_search: { one: "Busqu\xE9 en la web", many: "Busqu\xE9 en la web" },
76
+ web_fetch: { one: "Abr\xED una p\xE1gina", many: "Abr\xED %n p\xE1ginas" },
77
+ fetch: { one: "Abr\xED una p\xE1gina", many: "Abr\xED %n p\xE1ginas" },
78
+ site_publish: { one: "Publiqu\xE9 una p\xE1gina", many: "Publiqu\xE9 %n p\xE1ginas" },
79
+ pdf: { one: "Gener\xE9 un PDF", many: "Gener\xE9 %n PDF" },
80
+ connector: { one: "Us\xE9 tu sistema", many: "Us\xE9 tu sistema %n veces" },
81
+ mcp: { one: "Us\xE9 una herramienta", many: "Us\xE9 %n herramientas" },
82
+ browser: { one: "Us\xE9 el navegador", many: "Us\xE9 el navegador" },
83
+ todo: { one: "Anot\xE9 el plan", many: "Anot\xE9 el plan" },
84
+ delete: { one: "Borr\xE9 un archivo", many: "Borr\xE9 %n archivos" },
85
+ move: { one: "Mov\xED un archivo", many: "Mov\xED %n archivos" },
86
+ think: { one: "Lo pens\xE9", many: "Lo pens\xE9" },
87
+ image: { one: "Cre\xE9 una imagen", many: "Cre\xE9 %n im\xE1genes" },
88
+ delegate: { one: "Delegu\xE9 una tarea", many: "Delegu\xE9 %n tareas" },
89
+ collect: { one: "Recog\xED un resultado", many: "Recog\xED %n resultados" },
90
+ status: { one: "Pregunt\xE9 c\xF3mo iba", many: "Pregunt\xE9 %n veces c\xF3mo iba" },
91
+ stop: { one: "Detuve una tarea", many: "Detuve %n tareas" },
92
+ guide: { one: "Le\xED una gu\xEDa", many: "Le\xED %n gu\xEDas" },
93
+ deliver: { one: "Entregu\xE9 un archivo", many: "Entregu\xE9 %n archivos" },
94
+ sheet: { one: "Consult\xE9 una hoja", many: "Consult\xE9 %n hojas" },
95
+ view: { one: "Arm\xE9 una vista", many: "Arm\xE9 %n vistas" }
96
+ };
97
+ var TOOL_VERBS_EN = {
98
+ read: { one: "Read a file", many: "Read %n files" },
99
+ read_file: { one: "Read a file", many: "Read %n files" },
100
+ write: { one: "Wrote a file", many: "Wrote %n files" },
101
+ write_file: { one: "Wrote a file", many: "Wrote %n files" },
102
+ edit: { one: "Edited a file", many: "Edited %n files" },
103
+ edit_file: { one: "Edited a file", many: "Edited %n files" },
104
+ bash: { one: "Ran a command", many: "Ran %n commands" },
105
+ shell: { one: "Ran a command", many: "Ran %n commands" },
106
+ terminal: { one: "Ran a command", many: "Ran %n commands" },
107
+ execute: { one: "Ran a command", many: "Ran %n commands" },
108
+ grep: { one: "Searched the code", many: "Searched the code" },
109
+ glob: { one: "Searched for files", many: "Searched for files" },
110
+ search: { one: "Searched", many: "Searched %n times" },
111
+ tool_search: { one: "Looked for a tool", many: "Looked for %n tools" },
112
+ web_search: { one: "Searched the web", many: "Searched the web" },
113
+ web_fetch: { one: "Opened a page", many: "Opened %n pages" },
114
+ fetch: { one: "Opened a page", many: "Opened %n pages" },
115
+ site_publish: { one: "Published a page", many: "Published %n pages" },
116
+ pdf: { one: "Generated a PDF", many: "Generated %n PDFs" },
117
+ connector: { one: "Used your system", many: "Used your system %n times" },
118
+ mcp: { one: "Used a tool", many: "Used %n tools" },
119
+ browser: { one: "Used the browser", many: "Used the browser" },
120
+ todo: { one: "Wrote down the plan", many: "Wrote down the plan" },
121
+ delete: { one: "Deleted a file", many: "Deleted %n files" },
122
+ move: { one: "Moved a file", many: "Moved %n files" },
123
+ think: { one: "Thought it through", many: "Thought it through" },
124
+ image: { one: "Created an image", many: "Created %n images" },
125
+ delegate: { one: "Delegated a task", many: "Delegated %n tasks" },
126
+ collect: { one: "Collected a result", many: "Collected %n results" },
127
+ status: { one: "Checked how it was going", many: "Checked %n times how it was going" },
128
+ stop: { one: "Stopped a task", many: "Stopped %n tasks" },
129
+ guide: { one: "Read a guide", many: "Read %n guides" },
130
+ deliver: { one: "Delivered a file", many: "Delivered %n files" },
131
+ sheet: { one: "Checked a sheet", many: "Checked %n sheets" },
132
+ view: { one: "Built a view", many: "Built %n views" }
133
+ };
134
+
135
+ export {
136
+ isRunning,
137
+ toolNames,
138
+ toolName,
139
+ stepLabel,
140
+ summarizeTools,
141
+ groupSteps,
142
+ TOOL_VERBS_ES,
143
+ TOOL_VERBS_EN
144
+ };