@classic-homes/agent-chat 0.12.0 → 0.13.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
@@ -78,15 +78,22 @@ loads produced files with `client.fetchFile` (blob URLs, revoked when the messag
78
78
  A host can show the chat over its pages instead of on a route of its own:
79
79
 
80
80
  - `createChatSurface({ storageKey })` holds which presentation is showing (`launcher`, `float`
81
- or `dock`), the launcher's corner, the window's geometry and an `unread` flag; it persists to
82
- `localStorage` under `storageKey` and keeps the window inside the viewport.
83
- - `ChatLauncher` is the floating button (drag to another corner; shows answering, unseen answer,
84
- error and a MOCK marker). `FloatingWindow` is a non-modal window around any content: drag by
85
- the header, resize from the edges, drop on the right edge to dock, arrow keys on the move
86
- handle, Escape to minimize. The dock itself is the host's (e.g. a theme `DataPanel`).
81
+ or `dock`), where the launcher sits (`corner`, or `launcher` once the user has placed it), the
82
+ window's geometry and an `unread` flag; it persists to `localStorage` under `storageKey` and
83
+ keeps both inside the viewport. The launcher's place and the window's are separate.
84
+ - `ChatLauncher` is the floating button. The user can drag it anywhere; pass `position` and save
85
+ the drop from `onmove` (`surface.setLauncher`). While answering an arc orbits it; it also shows
86
+ an unseen answer, an error and a MOCK marker.
87
+ - `FloatingWindow` is a non-modal window around any content: one header (mark, `title`, optional
88
+ `subtitle` such as the conversation name, your `actions`, Dock, Minimize), drag by the header,
89
+ resize from the edges, drop on the right edge to dock, arrow keys on the move handle, Escape to
90
+ minimize. It takes the keyboard when it appears (the first text field, else the window;
91
+ `autofocus={false}` to opt out). The dock itself is the host's (e.g. a theme `DataPanel`).
92
+ For history, render `ConversationSidebar` in place of the chat (`class="w-full border-r-0"`);
93
+ with a host-owned session nothing is lost while it shows.
87
94
  - Pass `AgentChat` a host-owned `session` (`new ChatSession({...})`) and `variant="float"` or
88
95
  `"panel"`. The session then survives the view moving between presentations, including a
89
- running answer. With `session` set, configure `mode`, `agent`, `clientKind`, `getContext` and
96
+ running answer and the unsent composer text (`session.draft`). With `session` set, configure `mode`, `agent`, `clientKind`, `getContext` and
90
97
  the turn callbacks on the session; those `AgentChat` props are ignored. Call
91
98
  `surface.notifyTurnEnd()` from the session's `onTurnEnd` to flag an answer that finished while
92
99
  collapsed.
@@ -118,4 +125,7 @@ A host can show the chat over its pages instead of on a route of its own:
118
125
  an error with Try again when that load fails.
119
126
  - `ArtifactPanel`, `UiFrame`, `ModeBanner`.
120
127
 
128
+ While an answer has nothing to show yet, the assistant message cycles through
129
+ `THINKING_PHRASES` (homebuilding verbs, as in the Classic Chat widget) beside a stepped loader.
130
+
121
131
  Publishing: `docs/operations/package-publishing.md`.
@@ -185,7 +185,6 @@
185
185
  });
186
186
 
187
187
  let composer: Composer | undefined = $state();
188
- let draft = $state('');
189
188
  let files = $state<FileRef[]>([]);
190
189
  let selectedArtifact = $state<string | null>(null);
191
190
  let artifactsOpen = $state(false);
@@ -248,7 +247,7 @@
248
247
  if (conversationId) void session.load(conversationId);
249
248
  }
250
249
  function prompt(text: string): void {
251
- draft = text;
250
+ session.draft = text;
252
251
  composer?.focus();
253
252
  }
254
253
  </script>
@@ -354,7 +353,7 @@
354
353
  />
355
354
  <Composer
356
355
  bind:this={composer}
357
- bind:value={draft}
356
+ bind:value={session.draft}
358
357
  bind:files
359
358
  disabled={session.status === 'loading'}
360
359
  streaming={session.streaming}
@@ -1 +1 @@
1
- {"version":3,"file":"AgentChat.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AgentChat.svelte.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AgentChat.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AgentChat.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAClE,OAAO,KAAK,EACR,QAAQ,EACR,YAAY,EACZ,UAAU,EACV,eAAe,EAEhB,MAAM,gCAAgC,CAAC;AAC1C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAclE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACnD,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAQrD,UAAU,KAAK;IACb,MAAM,EAAE,cAAc,CAAC;IACvB;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,oEAAoE;IACpE,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,+EAA+E;IAC/E,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,yEAAyE;IACzE,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC;IACxB,8FAA8F;IAC9F,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAC;IAC1C,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CAAC;IACrC,UAAU,CAAC,EAAE,MAAM,eAAe,GAAG,IAAI,GAAG,SAAS,CAAC;IACtD,qBAAqB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;IACzD,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACtD,gFAAgF;IAChF,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE;QACjB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,MAAM,EAAE,MAAM,CAAC;KAChB,KAAK,IAAI,CAAC;IACX,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,iGAAiG;IACjG,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC7E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,OAAO,CAAC,EAAE,cAAc,EAAE,CAAC;IAC3B,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,6EAA6E;IAC7E,WAAW,CAAC,EAAE,iBAAiB,EAAE,CAAC;IAClC,mDAAmD;IACnD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8CAA8C;IAC9C,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,6FAA6F;IAC7F,MAAM,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC,CAAC;QAAE,KAAK,EAAE,QAAQ,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IAC7E,OAAO,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACnC,+DAA+D;IAC/D,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8QH,QAAA,MAAM,SAAS,2CAAwC,CAAC;AACxD,KAAK,SAAS,GAAG,UAAU,CAAC,OAAO,SAAS,CAAC,CAAC;AAC9C,eAAe,SAAS,CAAC"}
@@ -9,6 +9,7 @@
9
9
  import type { AssistantMessage } from '../reducer.js';
10
10
  import { errorTitle, isContinuable } from '../reducer.js';
11
11
  import { parseMarkdown } from '../markdown.js';
12
+ import { THINKING_PHRASE_MS, pickThinkingPhrase } from '../thinking.js';
12
13
  import ArtifactChip from './ArtifactChip.svelte';
13
14
  import Markdown from './Markdown.svelte';
14
15
  import MessageActions from './MessageActions.svelte';
@@ -101,6 +102,13 @@
101
102
  !message.tools.some((t) => !t.status) &&
102
103
  !message.artifacts.some((a) => a.open),
103
104
  );
105
+ // A homebuilding verb while there is nothing to show yet, changed every few seconds.
106
+ let phrase = $state(pickThinkingPhrase());
107
+ $effect(() => {
108
+ if (!thinking) return;
109
+ const timer = setInterval(() => (phrase = pickThinkingPhrase(phrase)), THINKING_PHRASE_MS);
110
+ return () => clearInterval(timer);
111
+ });
104
112
  // Continue carries on the text so far of an answer cut short or stopped; a reply stopped before
105
113
  // any text, or one that failed or lost its connection, is retried instead.
106
114
  const canContinue = $derived(last && !!oncontinue && isContinuable(message));
@@ -124,17 +132,15 @@
124
132
  {/if}
125
133
  {#if html}<Markdown {html} streaming={streaming && !!message.text} onclick={handleClick} />{/if}
126
134
  {#if thinking}
127
- <div class="flex items-center gap-2 text-sm text-muted-foreground" role="status">
128
- <span class="flex gap-1" aria-hidden="true">
129
- <span class="h-1.5 w-1.5 rounded-full bg-current motion-safe:animate-bounce"></span>
130
- <span
131
- class="h-1.5 w-1.5 rounded-full bg-current motion-safe:animate-bounce [animation-delay:150ms]"
132
- ></span>
133
- <span
134
- class="h-1.5 w-1.5 rounded-full bg-current motion-safe:animate-bounce [animation-delay:300ms]"
135
- ></span>
136
- </span>
137
- Thinking
135
+ <!-- aria-live off: the phrase changes for the eye; announcing each one would be noise. -->
136
+ <div
137
+ class="flex items-center gap-2 text-sm italic text-muted-foreground"
138
+ role="status"
139
+ aria-live="off"
140
+ data-agent-thinking
141
+ >
142
+ <span>{phrase}</span>
143
+ <span class="thinking-loader" aria-hidden="true"></span>
138
144
  </div>
139
145
  {/if}
140
146
  {#if message.artifacts.length}
@@ -204,3 +210,30 @@
204
210
  {/if}
205
211
  </div>
206
212
  </div>
213
+
214
+ <style>
215
+ /* Six blocks with one lit, stepping along (the Classic Chat widget's loader). Only custom
216
+ properties and an animation here: nothing a host's base styles compete with. */
217
+ .thinking-loader {
218
+ display: inline-block;
219
+ width: 60px;
220
+ height: 10px;
221
+ flex-shrink: 0;
222
+ -webkit-mask: linear-gradient(90deg, #000 70%, #0000 0) left / 20% 100%;
223
+ mask: linear-gradient(90deg, #000 70%, #0000 0) left / 20% 100%;
224
+ background: linear-gradient(hsl(var(--primary)) 0 0) left -25% top 0 / 20% 100% no-repeat
225
+ hsl(var(--muted-foreground) / 0.25);
226
+ animation: thinking-slide 1s infinite steps(6);
227
+ }
228
+ @keyframes thinking-slide {
229
+ 100% {
230
+ background-position: right -25% top 0;
231
+ }
232
+ }
233
+ @media (prefers-reduced-motion: reduce) {
234
+ .thinking-loader {
235
+ animation: none;
236
+ background-position: left 37.5% top 0;
237
+ }
238
+ }
239
+ </style>
@@ -1 +1 @@
1
- {"version":3,"file":"AssistantMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AssistantMessage.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAYpD,UAAU,KAAK;IACb,OAAO,EAAE,gBAAgB,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,gGAAgG;IAChG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,iDAAiD;IACjD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAmKH;;;GAGG;AACH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"AssistantMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AssistantMessage.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAapD,UAAU,KAAK;IACb,OAAO,EAAE,gBAAgB,CAAC;IAC1B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,gGAAgG;IAChG,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,cAAc,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9C,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IACtE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,8EAA8E;IAC9E,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,iDAAiD;IACjD,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/E,8DAA8D;IAC9D,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AA0KH;;;GAGG;AACH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
@@ -1,16 +1,20 @@
1
1
  <script lang="ts">
2
2
  /**
3
- * The floating launcher: a round button pinned to a viewport corner that opens the chat. The
4
- * user can drag it; on release it snaps to the nearest corner. It also reports what the chat
3
+ * The floating launcher: a round button that opens the chat. It starts in a viewport corner;
4
+ * the user can drag it anywhere and it stays where it is dropped (kept inside the viewport),
5
+ * independent of where the chat window sits. It also reports what the chat
5
6
  * is doing while collapsed (answering, an unseen answer, an error) and keeps the mock backend
6
7
  * visible (AGENTS.md: mock must always be visible).
7
8
  */
8
9
  import { cn, Icon } from '@classic-homes/theme-svelte';
9
10
  import type { LauncherStatus } from '../surface.svelte.js';
10
- import { DRAG_THRESHOLD, nearestCorner, type Corner, type Point } from '../surface-geometry.js';
11
+ import { DRAG_THRESHOLD, clampLauncher, type Corner, type Point } from '../surface-geometry.js';
11
12
 
12
13
  interface Props {
14
+ /** Where it sits until the user places it. */
13
15
  corner?: Corner;
16
+ /** Where the user placed it (top-left, viewport px); null or absent uses `corner`. */
17
+ position?: Point | null;
14
18
  status?: LauncherStatus;
15
19
  /** The backend is the mock: show the marker on the button. */
16
20
  mock?: boolean;
@@ -20,12 +24,13 @@
20
24
  expanded?: boolean;
21
25
  draggable?: boolean;
22
26
  onclick?: () => void;
23
- /** The user dropped the button in another corner. */
24
- oncornerchange?: (corner: Corner) => void;
27
+ /** The user dropped the button here (top-left, already inside the viewport). */
28
+ onmove?: (position: Point) => void;
25
29
  class?: string;
26
30
  }
27
31
  let {
28
32
  corner = 'bottom-right',
33
+ position = null,
29
34
  status = 'idle',
30
35
  mock = false,
31
36
  label = 'Classic Agent',
@@ -33,7 +38,7 @@
33
38
  expanded = false,
34
39
  draggable = true,
35
40
  onclick,
36
- oncornerchange,
41
+ onmove,
37
42
  class: className,
38
43
  }: Props = $props();
39
44
 
@@ -59,6 +64,11 @@
59
64
  'top-left': 'top-5 left-5',
60
65
  };
61
66
 
67
+ const viewport = () => ({ width: window.innerWidth, height: window.innerHeight });
68
+ // A placed button has to stay reachable when the window shrinks.
69
+ let size = $state(typeof window === 'undefined' ? { width: 1280, height: 800 } : viewport());
70
+ const placed = $derived(position ? clampLauncher(position, size) : null);
71
+
62
72
  let start: Point | null = null;
63
73
  let offset = $state<Point>({ x: 0, y: 0 });
64
74
  let dragging = $state(false);
@@ -85,12 +95,10 @@
85
95
  if (!dragging) return;
86
96
  dragging = false;
87
97
  suppressClick = true;
98
+ // Where the button is now, drag offset included.
99
+ const box = button?.getBoundingClientRect();
88
100
  offset = { x: 0, y: 0 };
89
- const next = nearestCorner(
90
- { x: event.clientX, y: event.clientY },
91
- { width: window.innerWidth, height: window.innerHeight },
92
- );
93
- if (next !== corner) oncornerchange?.(next);
101
+ if (box) onmove?.(clampLauncher({ x: box.left, y: box.top }, viewport()));
94
102
  }
95
103
  function onpointercancel(): void {
96
104
  start = null;
@@ -106,15 +114,19 @@
106
114
  }
107
115
  </script>
108
116
 
117
+ <svelte:window onresize={() => (size = viewport())} />
118
+
109
119
  <button
110
120
  bind:this={button}
111
121
  type="button"
112
122
  class={cn(
113
123
  'fixed z-40 flex size-14 touch-none select-none items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg outline-none ring-offset-background transition-shadow hover:shadow-xl focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
114
- CORNER_CLASS[corner],
115
- dragging ? 'cursor-grabbing opacity-80' : 'cursor-pointer',
124
+ !placed && CORNER_CLASS[corner],
125
+ dragging ? 'scale-105 cursor-grabbing opacity-85' : 'cursor-pointer',
116
126
  className,
117
127
  )}
128
+ style:left={placed ? `${placed.x}px` : undefined}
129
+ style:top={placed ? `${placed.y}px` : undefined}
118
130
  style:transform={dragging ? `translate(${offset.x}px, ${offset.y}px)` : undefined}
119
131
  aria-label={name}
120
132
  aria-expanded={expanded}
@@ -129,12 +141,18 @@
129
141
  {onpointercancel}
130
142
  >
131
143
  {#if status === 'streaming'}
144
+ <!-- Answering: an arc orbits just outside the button and the icon breathes. With reduced motion
145
+ the rim stays as a still ring. -->
132
146
  <span
133
- class="pointer-events-none absolute inset-0 animate-ping rounded-full bg-primary opacity-30 motion-reduce:hidden"
147
+ class="pointer-events-none absolute -inset-[7px] rounded-full animate-spin [animation-duration:1.1s] motion-reduce:animate-none"
148
+ style="background: conic-gradient(from 0deg, transparent 0 35%, hsl(var(--primary)) 100%); -webkit-mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2.5px)); mask: radial-gradient(farthest-side, transparent calc(100% - 3px), #000 calc(100% - 2.5px));"
134
149
  aria-hidden="true"
150
+ data-testid="launcher-ring"
135
151
  ></span>
136
152
  {/if}
137
- <Icon name="sparkles" size={24} />
153
+ <span class={cn('flex', status === 'streaming' && 'motion-safe:animate-pulse')}>
154
+ <Icon name="sparkles" size={24} />
155
+ </span>
138
156
  {#if status === 'unread' || status === 'error'}
139
157
  <span
140
158
  class={cn(
@@ -1,7 +1,10 @@
1
1
  import type { LauncherStatus } from '../surface.svelte.js';
2
- import { type Corner } from '../surface-geometry.js';
2
+ import { type Corner, type Point } from '../surface-geometry.js';
3
3
  interface Props {
4
+ /** Where it sits until the user places it. */
4
5
  corner?: Corner;
6
+ /** Where the user placed it (top-left, viewport px); null or absent uses `corner`. */
7
+ position?: Point | null;
5
8
  status?: LauncherStatus;
6
9
  /** The backend is the mock: show the marker on the button. */
7
10
  mock?: boolean;
@@ -11,8 +14,8 @@ interface Props {
11
14
  expanded?: boolean;
12
15
  draggable?: boolean;
13
16
  onclick?: () => void;
14
- /** The user dropped the button in another corner. */
15
- oncornerchange?: (corner: Corner) => void;
17
+ /** The user dropped the button here (top-left, already inside the viewport). */
18
+ onmove?: (position: Point) => void;
16
19
  class?: string;
17
20
  }
18
21
  declare const ChatLauncher: import("svelte").Component<Props, {
@@ -1 +1 @@
1
- {"version":3,"file":"ChatLauncher.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ChatLauncher.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAiC,KAAK,MAAM,EAAc,MAAM,wBAAwB,CAAC;AAG9F,UAAU,KAAK;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qDAAqD;IACrD,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkHH,QAAA,MAAM,YAAY;iBA7FG,IAAI;MA6FiC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"ChatLauncher.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ChatLauncher.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAiC,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAG9F,UAAU,KAAK;IACb,8CAA8C;IAC9C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,sFAAsF;IACtF,QAAQ,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC;IACxB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,gFAAgF;IAChF,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,KAAK,KAAK,IAAI,CAAC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA2HH,QAAA,MAAM,YAAY;iBArGG,IAAI;MAqGiC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -5,7 +5,7 @@
5
5
  * it is given. Keyboard: the move handle takes arrow keys (Shift resizes, Alt for small
6
6
  * steps); Escape inside the window minimizes it unless something inside used the key.
7
7
  */
8
- import { untrack, type Snippet } from 'svelte';
8
+ import { onMount, untrack, type Snippet } from 'svelte';
9
9
  import { Button, cn, Icon } from '@classic-homes/theme-svelte';
10
10
  import type { ChatSurface } from '../surface.svelte.js';
11
11
  import {
@@ -20,6 +20,10 @@
20
20
  interface Props {
21
21
  surface: ChatSurface;
22
22
  title?: string;
23
+ /** A second, quieter line under the title, e.g. the conversation's name. */
24
+ subtitle?: string;
25
+ /** Replaces the mark before the title. */
26
+ icon?: Snippet;
23
27
  id?: string;
24
28
  /** Header buttons before Dock and Minimize, e.g. New chat. */
25
29
  actions?: Snippet;
@@ -29,17 +33,25 @@
29
33
  resizable?: boolean;
30
34
  /** Called after the window minimizes, e.g. to move focus to the launcher. */
31
35
  onminimize?: () => void;
36
+ /**
37
+ * Take the keyboard when the window appears and nothing inside has it yet: the first text
38
+ * field, or the window itself. Without it Escape and Tab start from the page underneath.
39
+ */
40
+ autofocus?: boolean;
32
41
  class?: string;
33
42
  }
34
43
  let {
35
44
  surface,
36
45
  title = 'Classic Agent',
46
+ subtitle,
47
+ icon,
37
48
  id = 'agent-chat-window',
38
49
  actions,
39
50
  children,
40
51
  dockable = true,
41
52
  resizable = true,
42
53
  onminimize,
54
+ autofocus = true,
43
55
  class: className,
44
56
  }: Props = $props();
45
57
 
@@ -47,6 +59,16 @@
47
59
  // Untracked: attaching clamps the saved rect, which must not re-run this effect.
48
60
  $effect(() => untrack(() => surface.attach()));
49
61
 
62
+ let root: HTMLElement | undefined = $state();
63
+ // After the content has mounted (and had its own chance to focus something).
64
+ onMount(() => {
65
+ if (!autofocus || !root || root.contains(document.activeElement)) return;
66
+ const field = root.querySelector<HTMLElement>(
67
+ 'textarea:not([disabled]), input:not([disabled])',
68
+ );
69
+ (field ?? root).focus({ preventScroll: true });
70
+ });
71
+
50
72
  function minimize(): void {
51
73
  surface.minimize();
52
74
  onminimize?.();
@@ -161,14 +183,16 @@
161
183
  }
162
184
  </script>
163
185
 
164
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
186
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions, a11y_no_noninteractive_element_to_interactive_role -->
165
187
  <section
188
+ bind:this={root}
166
189
  {id}
190
+ tabindex="-1"
167
191
  role="dialog"
168
192
  aria-modal="false"
169
193
  aria-labelledby="{id}-title"
170
194
  class={cn(
171
- 'fixed z-40 flex flex-col overflow-hidden rounded-xl border border-border bg-background text-foreground shadow-2xl',
195
+ 'fixed z-40 flex flex-col overflow-hidden rounded-xl border border-border bg-background text-foreground shadow-2xl outline-none',
172
196
  surface.moving && 'select-none',
173
197
  dockHint && 'ring-2 ring-primary',
174
198
  className,
@@ -184,7 +208,7 @@
184
208
  <!-- svelte-ignore a11y_no_static_element_interactions -->
185
209
  <div
186
210
  class={cn(
187
- 'flex h-11 shrink-0 touch-none items-center gap-1 border-b border-border bg-muted/40 pl-1 pr-2',
211
+ 'flex h-12 shrink-0 touch-none items-center gap-1 border-b border-border bg-muted/40 pl-1 pr-2',
188
212
  surface.moving ? 'cursor-grabbing' : 'cursor-grab',
189
213
  )}
190
214
  data-testid="window-header"
@@ -204,7 +228,24 @@
204
228
  >
205
229
  <Icon name="grip" size={14} />
206
230
  </button>
207
- <h2 id="{id}-title" class="min-w-0 flex-1 truncate text-sm font-semibold">{title}</h2>
231
+ {#if icon}
232
+ {@render icon()}
233
+ {:else}
234
+ <span
235
+ class="flex size-7 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary"
236
+ aria-hidden="true"
237
+ >
238
+ <Icon name="sparkles" size={14} />
239
+ </span>
240
+ {/if}
241
+ <div class="ml-1 flex min-w-0 flex-1 flex-col leading-tight">
242
+ <h2 id="{id}-title" class="truncate text-sm font-semibold">{title}</h2>
243
+ {#if subtitle}
244
+ <span class="truncate text-[11px] text-muted-foreground" data-testid="window-subtitle"
245
+ >{subtitle}</span
246
+ >
247
+ {/if}
248
+ </div>
208
249
  {@render actions?.()}
209
250
  {#if dockable}
210
251
  <Button
@@ -9,6 +9,10 @@ import type { ChatSurface } from '../surface.svelte.js';
9
9
  interface Props {
10
10
  surface: ChatSurface;
11
11
  title?: string;
12
+ /** A second, quieter line under the title, e.g. the conversation's name. */
13
+ subtitle?: string;
14
+ /** Replaces the mark before the title. */
15
+ icon?: Snippet;
12
16
  id?: string;
13
17
  /** Header buttons before Dock and Minimize, e.g. New chat. */
14
18
  actions?: Snippet;
@@ -18,6 +22,11 @@ interface Props {
18
22
  resizable?: boolean;
19
23
  /** Called after the window minimizes, e.g. to move focus to the launcher. */
20
24
  onminimize?: () => void;
25
+ /**
26
+ * Take the keyboard when the window appears and nothing inside has it yet: the first text
27
+ * field, or the window itself. Without it Escape and Tab start from the page underneath.
28
+ */
29
+ autofocus?: boolean;
21
30
  class?: string;
22
31
  }
23
32
  declare const FloatingWindow: import("svelte").Component<Props, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"FloatingWindow.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/FloatingWindow.svelte.ts"],"names":[],"mappings":"AAGA;;;;;KAKK;AACL,OAAO,EAAW,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAE/C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWtD,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAkLH,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
1
+ {"version":3,"file":"FloatingWindow.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/FloatingWindow.svelte.ts"],"names":[],"mappings":"AAGA;;;;;KAKK;AACL,OAAO,EAAoB,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAExD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAWtD,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,yEAAyE;IACzE,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA2MH,QAAA,MAAM,cAAc,2CAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
package/dist/index.d.ts CHANGED
@@ -8,6 +8,7 @@ export * from './session.svelte.js';
8
8
  export * from './shortcuts.js';
9
9
  export * from './surface-geometry.js';
10
10
  export * from './surface.svelte.js';
11
+ export * from './thinking.js';
11
12
  export * from './ui-frame-protocol.js';
12
13
  export * from './welcome.js';
13
14
  export { default as AgentChat } from './components/AgentChat.svelte';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AACzF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,oCAAoC,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,wBAAwB,CAAC;AACvC,cAAc,cAAc,CAAC;AAC7B,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,mCAAmC,CAAC;AAC7E,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AACzF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,iCAAiC,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,oCAAoC,CAAC;AAC/E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,6BAA6B,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,kCAAkC,CAAC;AAC3E,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,oCAAoC,CAAC"}
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ export * from './session.svelte.js';
8
8
  export * from './shortcuts.js';
9
9
  export * from './surface-geometry.js';
10
10
  export * from './surface.svelte.js';
11
+ export * from './thinking.js';
11
12
  export * from './ui-frame-protocol.js';
12
13
  export * from './welcome.js';
13
14
  export { default as AgentChat } from './components/AgentChat.svelte';
@@ -50,6 +50,8 @@ export declare class ChatSession {
50
50
  bytes: number;
51
51
  status: "uploaded" | "indexed" | "failed";
52
52
  }[];
53
+ /** The unsent composer text, kept here so it survives the view being remounted. */
54
+ draft: string;
53
55
  private controller;
54
56
  /** The request being streamed; differs from the shown answer's id while continuing it. */
55
57
  private streamRequestId;
@@ -1 +1 @@
1
- {"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"session.svelte.d.ts","sourceRoot":"","sources":["../src/lib/session.svelte.ts"],"names":[],"mappings":"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"}
@@ -18,6 +18,8 @@ export class ChatSession {
18
18
  agent = $state('classic-agent');
19
19
  loadError = $state(undefined);
20
20
  pendingFiles = $state([]);
21
+ /** The unsent composer text, kept here so it survives the view being remounted. */
22
+ draft = $state('');
21
23
  controller;
22
24
  /** The request being streamed; differs from the shown answer's id while continuing it. */
23
25
  streamRequestId;
@@ -44,6 +44,10 @@ export declare function clampSize(size: Size, viewport: Size): Size;
44
44
  export declare function clampRect(rect: Rect, viewport: Size): Rect;
45
45
  /** Where a new window opens: beside the launcher's corner, clear of the launcher itself. */
46
46
  export declare function defaultRect(viewport: Size, corner: Corner, size?: Size): Rect;
47
+ /** Top-left point of the launcher button when it sits in `corner`. */
48
+ export declare function launcherPoint(corner: Corner, viewport: Size): Point;
49
+ /** Keep a freely placed launcher button fully inside the viewport. */
50
+ export declare function clampLauncher(point: Point, viewport: Size): Point;
47
51
  /** The corner a dropped launcher snaps to. */
48
52
  export declare function nearestCorner(point: Point, viewport: Size): Corner;
49
53
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"surface-geometry.d.ts","sourceRoot":"","sources":["../src/lib/surface-geometry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AACD,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AACD,MAAM,WAAW,IAAK,SAAQ,KAAK,EAAE,IAAI;CAAG;AAE5C,MAAM,MAAM,MAAM,GAAG,cAAc,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAC/E,eAAO,MAAM,OAAO,EAAE,SAAS,MAAM,EAA6D,CAAC;AAEnG,2CAA2C;AAC3C,MAAM,MAAM,eAAe,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAChF,eAAO,MAAM,iBAAiB,EAAE,SAAS,eAAe,EASvD,CAAC;AAEF,eAAO,MAAM,YAAY;;;;;IAKvB,yDAAyD;;CAEjD,CAAC;AACX,eAAO,MAAM,kBAAkB,EAAE,IAAkC,CAAC;AACpE,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,gGAAgG;AAChG,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,yFAAyF;AACzF,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,4EAA4E;AAC5E,eAAO,MAAM,QAAQ,KAAK,CAAC;AAK3B,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAG,IAAI,CAS1D;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAG,IAAI,CAQ1D;AAED,4FAA4F;AAC5F,wBAAgB,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,IAAyB,GAAG,IAAI,CAmBjG;AAED,8CAA8C;AAC9C,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,MAAM,CAIlE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,eAAe,EAC1B,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,IAAI,GACb,IAAI,CAaN;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,OAAO,CAElE"}
1
+ {"version":3,"file":"surface-geometry.d.ts","sourceRoot":"","sources":["../src/lib/surface-geometry.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,WAAW,KAAK;IACpB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AACD,MAAM,WAAW,IAAI;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AACD,MAAM,WAAW,IAAK,SAAQ,KAAK,EAAE,IAAI;CAAG;AAE5C,MAAM,MAAM,MAAM,GAAG,cAAc,GAAG,aAAa,GAAG,WAAW,GAAG,UAAU,CAAC;AAC/E,eAAO,MAAM,OAAO,EAAE,SAAS,MAAM,EAA6D,CAAC;AAEnG,2CAA2C;AAC3C,MAAM,MAAM,eAAe,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAChF,eAAO,MAAM,iBAAiB,EAAE,SAAS,eAAe,EASvD,CAAC;AAEF,eAAO,MAAM,YAAY;;;;;IAKvB,yDAAyD;;CAEjD,CAAC;AACX,eAAO,MAAM,kBAAkB,EAAE,IAAkC,CAAC;AACpE,eAAO,MAAM,aAAa,KAAK,CAAC;AAChC,eAAO,MAAM,eAAe,KAAK,CAAC;AAClC,gGAAgG;AAChG,eAAO,MAAM,cAAc,IAAI,CAAC;AAChC,yFAAyF;AACzF,eAAO,MAAM,cAAc,KAAK,CAAC;AACjC,4EAA4E;AAC5E,eAAO,MAAM,QAAQ,KAAK,CAAC;AAK3B,wEAAwE;AACxE,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAG,IAAI,CAS1D;AAED,0EAA0E;AAC1E,wBAAgB,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,GAAG,IAAI,CAQ1D;AAED,4FAA4F;AAC5F,wBAAgB,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,GAAE,IAAyB,GAAG,IAAI,CAmBjG;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,GAAG,KAAK,CAMnE;AAED,sEAAsE;AACtE,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,KAAK,CAIjE;AAED,8CAA8C;AAC9C,wBAAgB,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,MAAM,CAIlE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,EACV,SAAS,EAAE,eAAe,EAC1B,EAAE,EAAE,MAAM,EACV,EAAE,EAAE,MAAM,EACV,QAAQ,EAAE,IAAI,GACb,IAAI,CAaN;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,GAAG,OAAO,CAElE"}
@@ -63,6 +63,19 @@ export function defaultRect(viewport, corner, size = DEFAULT_FLOAT_SIZE) {
63
63
  y: corner.startsWith('top') ? clear : viewport.height - fitted.height - clear,
64
64
  }, viewport);
65
65
  }
66
+ /** Top-left point of the launcher button when it sits in `corner`. */
67
+ export function launcherPoint(corner, viewport) {
68
+ const far = (extent) => Math.max(0, extent - LAUNCHER_SIZE - LAUNCHER_MARGIN);
69
+ return {
70
+ x: corner.endsWith('left') ? LAUNCHER_MARGIN : far(viewport.width),
71
+ y: corner.startsWith('top') ? LAUNCHER_MARGIN : far(viewport.height),
72
+ };
73
+ }
74
+ /** Keep a freely placed launcher button fully inside the viewport. */
75
+ export function clampLauncher(point, viewport) {
76
+ const within = (value, extent) => Math.min(Math.max(0, value), Math.max(0, extent - LAUNCHER_SIZE));
77
+ return { x: within(point.x, viewport.width), y: within(point.y, viewport.height) };
78
+ }
66
79
  /** The corner a dropped launcher snaps to. */
67
80
  export function nearestCorner(point, viewport) {
68
81
  const vertical = point.y < viewport.height / 2 ? 'top' : 'bottom';
@@ -1,4 +1,4 @@
1
- import { type Corner, type Rect, type ResizeDirection, type Size } from './surface-geometry.js';
1
+ import { type Corner, type Point, type Rect, type ResizeDirection, type Size } from './surface-geometry.js';
2
2
  /**
3
3
  * How the chat is presented over a host page: collapsed to the launcher button, a floating
4
4
  * window, or docked to the side. The full page is a host route, not a mode here; a host on that
@@ -21,7 +21,7 @@ export interface ChatSurfaceOptions {
21
21
  }
22
22
  /**
23
23
  * Reactive state of the detached chat (Svelte 5 runes class): which presentation is showing,
24
- * the launcher's corner, the floating window's geometry and whether an answer arrived unseen.
24
+ * where the launcher sits, the floating window's geometry and whether an answer arrived unseen.
25
25
  * Layout survives reloads through `storageKey`; a saved window that no longer fits the viewport
26
26
  * is pulled back inside.
27
27
  */
@@ -30,6 +30,11 @@ export declare class ChatSurface {
30
30
  /** The open presentation the launcher returns to. */
31
31
  lastOpen: OpenSurfaceMode;
32
32
  corner: Corner;
33
+ /**
34
+ * Where the user dropped the launcher (its top-left), or null while it sits in `corner`.
35
+ * Separate from the window's place: moving one never moves the other.
36
+ */
37
+ launcher: Point | null;
33
38
  /** An answer finished while the chat was collapsed. Cleared on open. */
34
39
  unread: boolean;
35
40
  /** True while the user drags or resizes the window. */
@@ -48,6 +53,11 @@ export declare class ChatSurface {
48
53
  /** A turn ended; flag it when the user cannot see the answer. */
49
54
  notifyTurnEnd(): void;
50
55
  setCorner(corner: Corner): void;
56
+ /**
57
+ * The user dropped the launcher at `point` (top-left). It stays there; `corner` follows the
58
+ * nearest one, which is where a window that was never moved opens.
59
+ */
60
+ setLauncher(point: Point): void;
51
61
  /** Move the window by a pointer or key delta. Call `commit` when the gesture ends. */
52
62
  moveBy(dx: number, dy: number): void;
53
63
  resizeBy(direction: ResizeDirection, dx: number, dy: number): void;
@@ -1 +1 @@
1
- {"version":3,"file":"surface.svelte.d.ts","sourceRoot":"","sources":["../src/lib/surface.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,MAAM,EACX,KAAK,IAAI,EACT,KAAK,eAAe,EACpB,KAAK,IAAI,EACV,MAAM,uBAAuB,CAAC;AAE/B;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AACxD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;AAE/D,kEAAkE;AAClE,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEvE,MAAM,WAAW,kBAAkB;IACjC,mGAAmG;IACnG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC;IAC/C,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAiBD;;;;;GAKG;AACH,qBAAa,WAAW;IACtB,IAAI,cAAmC;IACvC,qDAAqD;IACrD,QAAQ,kBAAoC;IAC5C,MAAM,SAAkC;IACxC,wEAAwE;IACxE,MAAM,UAAiB;IACvB,uDAAuD;IACvD,MAAM,UAAiB;IACvB,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,GAAE,kBAAuB;IAO5C,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,yFAAyF;IACzF,IAAI,IAAI,IAAI,IAAI,CAGf;IAED,IAAI,CAAC,IAAI,GAAE,eAA+B,GAAG,IAAI;IAMjD,QAAQ,IAAI,IAAI;IAIhB,MAAM,IAAI,IAAI;IAId,IAAI,IAAI,IAAI;IAGZ,KAAK,IAAI,IAAI;IAIb,iEAAiE;IACjE,aAAa,IAAI,IAAI;IAIrB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK/B,sFAAsF;IACtF,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAIpC,QAAQ,CAAC,SAAS,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAGlE,oDAAoD;IACpD,MAAM,IAAI,IAAI;IAId,6EAA6E;IAC7E,SAAS,IAAI,IAAI;IAKjB;;;OAGG;IACH,MAAM,CAAC,MAAM,GAAE,IAAI,CAAC,MAAM,EAAE,kBAAkB,GAAG,qBAAqB,CAAU,GAAG,MAAM,IAAI;IAS7F,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,OAAO;IAgBf,OAAO,CAAC,OAAO;CAchB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAE/E"}
1
+ {"version":3,"file":"surface.svelte.d.ts","sourceRoot":"","sources":["../src/lib/surface.svelte.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,MAAM,EACX,KAAK,KAAK,EACV,KAAK,IAAI,EACT,KAAK,eAAe,EACpB,KAAK,IAAI,EACV,MAAM,uBAAuB,CAAC;AAE/B;;;;GAIG;AACH,MAAM,MAAM,WAAW,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AACxD,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;AAE/D,kEAAkE;AAClE,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEvE,MAAM,WAAW,kBAAkB;IACjC,mGAAmG;IACnG,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,OAAO,CAAC,EAAE,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,SAAS,CAAC,CAAC;IAC/C,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,eAAe,CAAC;IAC9B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAuBD;;;;;GAKG;AACH,qBAAa,WAAW;IACtB,IAAI,cAAmC;IACvC,qDAAqD;IACrD,QAAQ,kBAAoC;IAC5C,MAAM,SAAkC;IACxC;;;OAGG;IACH,QAAQ,eAA8B;IACtC,wEAAwE;IACxE,MAAM,UAAiB;IACvB,uDAAuD;IACvD,MAAM,UAAiB;IACvB,OAAO,CAAC,KAAK,CAA6B;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;gBAEjC,OAAO,GAAE,kBAAuB;IAO5C,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,yFAAyF;IACzF,IAAI,IAAI,IAAI,IAAI,CAGf;IAED,IAAI,CAAC,IAAI,GAAE,eAA+B,GAAG,IAAI;IAMjD,QAAQ,IAAI,IAAI;IAIhB,MAAM,IAAI,IAAI;IAId,IAAI,IAAI,IAAI;IAGZ,KAAK,IAAI,IAAI;IAIb,iEAAiE;IACjE,aAAa,IAAI,IAAI;IAIrB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAK/B;;;OAGG;IACH,WAAW,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAU/B,sFAAsF;IACtF,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAIpC,QAAQ,CAAC,SAAS,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAGlE,oDAAoD;IACpD,MAAM,IAAI,IAAI;IAId,6EAA6E;IAC7E,SAAS,IAAI,IAAI;IAKjB;;;OAGG;IACH,MAAM,CAAC,MAAM,GAAE,IAAI,CAAC,MAAM,EAAE,kBAAkB,GAAG,qBAAqB,CAAU,GAAG,MAAM,IAAI;IAS7F,OAAO,CAAC,QAAQ;IAMhB,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,OAAO;IAkBf,OAAO,CAAC,OAAO;CAehB;AAED,wBAAgB,iBAAiB,CAAC,OAAO,GAAE,kBAAuB,GAAG,WAAW,CAE/E"}
@@ -1,10 +1,13 @@
1
- import { CORNERS, clampRect, defaultRect, resizeRect, } from './surface-geometry.js';
1
+ import { CORNERS, LAUNCHER_SIZE, clampLauncher, clampRect, defaultRect, nearestCorner, resizeRect, } from './surface-geometry.js';
2
+ const isPoint = (value) => typeof value === 'object' &&
3
+ value !== null &&
4
+ ['x', 'y'].every((key) => Number.isFinite(value[key]));
2
5
  const isRect = (value) => typeof value === 'object' &&
3
6
  value !== null &&
4
7
  ['x', 'y', 'width', 'height'].every((key) => Number.isFinite(value[key]));
5
8
  /**
6
9
  * Reactive state of the detached chat (Svelte 5 runes class): which presentation is showing,
7
- * the launcher's corner, the floating window's geometry and whether an answer arrived unseen.
10
+ * where the launcher sits, the floating window's geometry and whether an answer arrived unseen.
8
11
  * Layout survives reloads through `storageKey`; a saved window that no longer fits the viewport
9
12
  * is pulled back inside.
10
13
  */
@@ -13,6 +16,11 @@ export class ChatSurface {
13
16
  /** The open presentation the launcher returns to. */
14
17
  lastOpen = $state('float');
15
18
  corner = $state('bottom-right');
19
+ /**
20
+ * Where the user dropped the launcher (its top-left), or null while it sits in `corner`.
21
+ * Separate from the window's place: moving one never moves the other.
22
+ */
23
+ launcher = $state(null);
16
24
  /** An answer finished while the chat was collapsed. Cleared on open. */
17
25
  unread = $state(false);
18
26
  /** True while the user drags or resizes the window. */
@@ -62,6 +70,17 @@ export class ChatSurface {
62
70
  }
63
71
  setCorner(corner) {
64
72
  this.corner = corner;
73
+ this.launcher = null;
74
+ this.persist();
75
+ }
76
+ /**
77
+ * The user dropped the launcher at `point` (top-left). It stays there; `corner` follows the
78
+ * nearest one, which is where a window that was never moved opens.
79
+ */
80
+ setLauncher(point) {
81
+ const viewport = this.viewport();
82
+ this.launcher = clampLauncher(point, viewport);
83
+ this.corner = nearestCorner({ x: this.launcher.x + LAUNCHER_SIZE / 2, y: this.launcher.y + LAUNCHER_SIZE / 2 }, viewport);
65
84
  this.persist();
66
85
  }
67
86
  /** Move the window by a pointer or key delta. Call `commit` when the gesture ends. */
@@ -130,6 +149,8 @@ export class ChatSurface {
130
149
  this.corner = data.corner;
131
150
  if (isRect(data.rect))
132
151
  this.saved = clampRect(data.rect, this.viewport());
152
+ if (isPoint(data.launcher))
153
+ this.launcher = clampLauncher({ x: data.launcher.x, y: data.launcher.y }, this.viewport());
133
154
  }
134
155
  catch {
135
156
  // Unreadable or blocked storage: start from the defaults.
@@ -143,6 +164,7 @@ export class ChatSurface {
143
164
  lastOpen: this.lastOpen,
144
165
  corner: this.corner,
145
166
  rect: this.saved,
167
+ launcher: this.launcher,
146
168
  };
147
169
  this.storage()?.setItem(this.options.storageKey, JSON.stringify(data));
148
170
  }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * What the assistant shows while it works and has nothing to say yet: homebuilding verbs in
3
+ * place of a bare "Thinking" (the wording the Classic Chat widget uses).
4
+ */
5
+ export declare const THINKING_PHRASES: readonly string[];
6
+ /** How long one phrase shows before the next. */
7
+ export declare const THINKING_PHRASE_MS = 4000;
8
+ /** A random phrase, never `previous` twice in a row. */
9
+ export declare function pickThinkingPhrase(previous?: string, random?: () => number): string;
10
+ //# sourceMappingURL=thinking.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"thinking.d.ts","sourceRoot":"","sources":["../src/lib/thinking.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,gBAAgB,EAAE,SAAS,MAAM,EAiD7C,CAAC;AAEF,iDAAiD;AACjD,eAAO,MAAM,kBAAkB,OAAO,CAAC;AAEvC,wDAAwD;AACxD,wBAAgB,kBAAkB,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,MAAM,GAAE,MAAM,MAAoB,GAAG,MAAM,CAGhG"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * What the assistant shows while it works and has nothing to say yet: homebuilding verbs in
3
+ * place of a bare "Thinking" (the wording the Classic Chat widget uses).
4
+ */
5
+ export const THINKING_PHRASES = [
6
+ 'Framing',
7
+ 'Drafting',
8
+ 'Surveying',
9
+ 'Grading',
10
+ 'Excavating',
11
+ 'Leveling',
12
+ 'Measuring',
13
+ 'Squaring',
14
+ 'Pouring footings',
15
+ 'Pouring slab',
16
+ 'Laying foundation',
17
+ 'Raising walls',
18
+ 'Setting trusses',
19
+ 'Sheathing',
20
+ 'Roofing',
21
+ 'Nailing',
22
+ 'Checking plans',
23
+ 'Reading blueprints',
24
+ 'Pulling permits',
25
+ 'Scheduling inspections',
26
+ 'Sketching layouts',
27
+ 'Mapping lots',
28
+ 'Staking corners',
29
+ 'Wiring',
30
+ 'Plumbing',
31
+ 'Ducting',
32
+ 'Insulating',
33
+ 'Hanging drywall',
34
+ 'Mudding',
35
+ 'Sanding',
36
+ 'Priming',
37
+ 'Painting',
38
+ 'Laying tile',
39
+ 'Installing cabinets',
40
+ 'Setting countertops',
41
+ 'Hanging doors',
42
+ 'Trimming',
43
+ 'Flooring',
44
+ 'Picking finishes',
45
+ 'Matching paint',
46
+ 'Choosing fixtures',
47
+ 'Landscaping',
48
+ 'Touring models',
49
+ 'Comparing plans',
50
+ 'Building',
51
+ 'Punch list',
52
+ 'Final walkthrough',
53
+ 'Handing keys',
54
+ ];
55
+ /** How long one phrase shows before the next. */
56
+ export const THINKING_PHRASE_MS = 4000;
57
+ /** A random phrase, never `previous` twice in a row. */
58
+ export function pickThinkingPhrase(previous, random = Math.random) {
59
+ const pool = previous ? THINKING_PHRASES.filter((p) => p !== previous) : THINKING_PHRASES;
60
+ return pool[Math.min(pool.length - 1, Math.floor(random() * pool.length))];
61
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classic-homes/agent-chat",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Svelte 5 chat UI for Classic Agent: session state, message list, composer, tool timeline, artifacts and MCP-UI sandbox frames. Consumed by the Classic App.",
@@ -18,8 +18,8 @@
18
18
  "!dist/**/*.test.*"
19
19
  ],
20
20
  "dependencies": {
21
- "@classic-homes/agent-client": "0.12.0",
22
- "@classic-homes/agent-contracts": "0.12.0",
21
+ "@classic-homes/agent-client": "0.13.0",
22
+ "@classic-homes/agent-contracts": "0.13.0",
23
23
  "dompurify": "3.4.17"
24
24
  },
25
25
  "peerDependencies": {