@haikit/client 0.4.0 → 0.6.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.6.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,14 @@ 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
+ * the conversation has begun, and a server without an init tool creates
118
+ * nothing. If init throws, nothing is recorded, so calling it again tries
119
+ * again. `mountChat` calls it when it opens and after `reset()`.
120
+ */
121
+ start(): Promise<boolean>;
114
122
  interact(handle: string, action: string, value: unknown): Promise<void>;
115
123
  /** Mounts the surface for `handle` into `element`. Null if props have not arrived. */
116
124
  mount(handle: string, element: HTMLElement): SurfaceInstance | null;
package/src/index.js CHANGED
@@ -332,6 +332,28 @@ export function createChat({ endpoint = "/hai", registry }) {
332
332
  return enqueue("/chat", { message: text });
333
333
  }
334
334
 
335
+ /**
336
+ * Whether the server has recorded this conversation's opening — init's call,
337
+ * or a first message — as its last `context` shows. Having a conversation id
338
+ * is not enough: `hello` arrives before init runs, and an init that throws
339
+ * leaves the id behind with an empty history.
340
+ */
341
+ const begun = () => state.context.messages.length > 0;
342
+
343
+ /**
344
+ * Start the conversation before the user types, so a server with an init
345
+ * tool runs it now and whatever it shows is there first. Does nothing once
346
+ * the conversation has begun. If init throws, the server records nothing,
347
+ * so calling this again tries again — as the first message would. A server
348
+ * without an init tool answers with an empty stream and creates nothing;
349
+ * the first message starts the conversation, as it always has. Resolves
350
+ * like send().
351
+ */
352
+ async function start() {
353
+ if (begun() || closed()) return false;
354
+ return enqueue("/start", {});
355
+ }
356
+
335
357
  // A click carries nothing the user authored, and stacking them is worse than
336
358
  // dropping them: a double-click on a picker row would resolve it and then
337
359
  // queue a second resolution into "component is frozen". The deadline is
@@ -365,6 +387,7 @@ export function createChat({ endpoint = "/hai", registry }) {
365
387
 
366
388
  return {
367
389
  state,
390
+ start,
368
391
  send,
369
392
  interact,
370
393
  mount,