@haikit/client 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haikit/client",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "The haikit browser runtime: streams tool payloads into interactive UI surfaces. Plain ESM, no build step, no dependencies.",
5
5
  "keywords": [
6
6
  "haikit",
package/src/app.d.ts CHANGED
@@ -17,7 +17,9 @@ export interface MountChatOptions {
17
17
 
18
18
  /**
19
19
  * Builds the shell, wires the composer, renders the transcript and mounts your
20
- * surfaces. The ten-line path to a running app.
20
+ * surfaces. The ten-line path to a running app. It starts the conversation as
21
+ * soon as it opens, and again after `reset()`, so a server with an init tool
22
+ * runs it before the user types.
21
23
  *
22
24
  * It is a convenience, not the API — everything it does is built on `createChat`
23
25
  * and `renderTranscript`. Dropping to those when this shell stops fitting is the
package/src/app.js CHANGED
@@ -96,7 +96,7 @@ export function mountChat({
96
96
  });
97
97
  };
98
98
 
99
- function renderEmpty() {
99
+ function emptyState() {
100
100
  const empty = h("div", "hai-empty");
101
101
  if (emptyText) empty.append(h("p", null, emptyText));
102
102
  if (suggestions.length) {
@@ -108,13 +108,27 @@ export function mountChat({
108
108
  }
109
109
  empty.append(row);
110
110
  }
111
- transcript.replaceChildren(empty);
111
+ return empty;
112
112
  }
113
113
 
114
+ const renderEmpty = () => transcript.replaceChildren(emptyState());
115
+
114
116
  // ── wiring ─────────────────────────────────────────────────────────
115
- chat.subscribe((state) => {
117
+ chat.subscribe((state, event) => {
118
+ // A new conversation starts at once, so the server's init tool — if it
119
+ // has one — runs before anything is typed.
120
+ if (event.type === "reset") chat.start();
121
+
116
122
  if (state.blocks.length === 0) renderEmpty();
117
- else renderTranscript(transcript, chat);
123
+ else {
124
+ renderTranscript(transcript, chat);
125
+ // what the conversation opened with (an init tool's row, a welcome
126
+ // surface) doesn't replace the suggestions; the user engaging does —
127
+ // a first message, or an answer to something init showed
128
+ if (!state.blocks.some((b) => b.kind === "user" || b.kind === "interaction")) {
129
+ transcript.append(emptyState());
130
+ }
131
+ }
118
132
 
119
133
  modelEl.textContent = state.model;
120
134
  input.disabled = state.expired !== null;
@@ -141,6 +155,7 @@ export function mountChat({
141
155
  };
142
156
 
143
157
  renderEmpty();
158
+ chat.start();
144
159
  return chat;
145
160
  }
146
161
 
package/src/index.d.ts CHANGED
@@ -111,6 +111,13 @@ export interface Chat {
111
111
  * cleared its input can then put the text back.
112
112
  */
113
113
  send(text: string): Promise<boolean>;
114
+ /**
115
+ * Start the conversation before the user types, so a server with an init
116
+ * tool runs it now and anything it shows is already there. Does nothing once
117
+ * this chat has a conversation, and a server without an init tool creates
118
+ * nothing. `mountChat` calls it when it opens and after `reset()`.
119
+ */
120
+ start(): Promise<boolean>;
114
121
  interact(handle: string, action: string, value: unknown): Promise<void>;
115
122
  /** Mounts the surface for `handle` into `element`. Null if props have not arrived. */
116
123
  mount(handle: string, element: HTMLElement): SurfaceInstance | null;
package/src/index.js CHANGED
@@ -332,6 +332,18 @@ export function createChat({ endpoint = "/hai", registry }) {
332
332
  return enqueue("/chat", { message: text });
333
333
  }
334
334
 
335
+ /**
336
+ * Start the conversation before the user types, so a server with an init
337
+ * tool runs it now and whatever it shows is there first. Does nothing once
338
+ * this chat has a conversation. A server without an init tool answers with
339
+ * an empty stream and creates nothing; the first message starts the
340
+ * conversation, as it always has. Resolves like send().
341
+ */
342
+ async function start() {
343
+ if (state.conversationId || closed()) return false;
344
+ return enqueue("/start", {});
345
+ }
346
+
335
347
  // A click carries nothing the user authored, and stacking them is worse than
336
348
  // dropping them: a double-click on a picker row would resolve it and then
337
349
  // queue a second resolution into "component is frozen". The deadline is
@@ -365,6 +377,7 @@ export function createChat({ endpoint = "/hai", registry }) {
365
377
 
366
378
  return {
367
379
  state,
380
+ start,
368
381
  send,
369
382
  interact,
370
383
  mount,