@classic-homes/agent-chat 0.1.2 → 0.2.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.
Files changed (64) hide show
  1. package/README.md +40 -0
  2. package/dist/components/AgentChat.svelte +65 -39
  3. package/dist/components/AgentChat.svelte.d.ts +2 -0
  4. package/dist/components/AgentChat.svelte.d.ts.map +1 -1
  5. package/dist/components/ArtifactPanel.svelte +79 -96
  6. package/dist/components/ArtifactPanel.svelte.d.ts +2 -0
  7. package/dist/components/ArtifactPanel.svelte.d.ts.map +1 -1
  8. package/dist/components/ChatHeader.svelte +122 -0
  9. package/dist/components/ChatHeader.svelte.d.ts +27 -0
  10. package/dist/components/ChatHeader.svelte.d.ts.map +1 -0
  11. package/dist/components/Composer.svelte +78 -121
  12. package/dist/components/Composer.svelte.d.ts.map +1 -1
  13. package/dist/components/ConversationDialogs.svelte +134 -0
  14. package/dist/components/ConversationDialogs.svelte.d.ts +25 -0
  15. package/dist/components/ConversationDialogs.svelte.d.ts.map +1 -0
  16. package/dist/components/ConversationSidebar.svelte +135 -114
  17. package/dist/components/ConversationSidebar.svelte.d.ts +8 -2
  18. package/dist/components/ConversationSidebar.svelte.d.ts.map +1 -1
  19. package/dist/components/Markdown.svelte +48 -0
  20. package/dist/components/Markdown.svelte.d.ts +16 -0
  21. package/dist/components/Markdown.svelte.d.ts.map +1 -0
  22. package/dist/components/MessageItem.svelte +27 -116
  23. package/dist/components/MessageItem.svelte.d.ts.map +1 -1
  24. package/dist/components/MessageList.svelte +23 -34
  25. package/dist/components/ModeBanner.svelte +7 -13
  26. package/dist/components/ModeBanner.svelte.d.ts.map +1 -1
  27. package/dist/components/SandboxFrame.svelte +43 -0
  28. package/dist/components/SandboxFrame.svelte.d.ts +13 -0
  29. package/dist/components/SandboxFrame.svelte.d.ts.map +1 -0
  30. package/dist/components/ToolTimeline.svelte +17 -68
  31. package/dist/components/ToolTimeline.svelte.d.ts.map +1 -1
  32. package/dist/components/UiFrame.svelte +9 -55
  33. package/dist/components/UiFrame.svelte.d.ts.map +1 -1
  34. package/dist/components/conversation-chrome.svelte.test.d.ts +2 -0
  35. package/dist/components/conversation-chrome.svelte.test.d.ts.map +1 -0
  36. package/dist/components/conversation-chrome.svelte.test.js +127 -0
  37. package/dist/components/markdown-prose.svelte.test.d.ts +2 -0
  38. package/dist/components/markdown-prose.svelte.test.d.ts.map +1 -0
  39. package/dist/components/markdown-prose.svelte.test.js +26 -0
  40. package/dist/composer-keys.d.ts +11 -0
  41. package/dist/composer-keys.d.ts.map +1 -0
  42. package/dist/composer-keys.js +10 -0
  43. package/dist/composer-keys.test.d.ts +2 -0
  44. package/dist/composer-keys.test.d.ts.map +1 -0
  45. package/dist/composer-keys.test.js +16 -0
  46. package/dist/index.d.ts +2 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +2 -0
  49. package/dist/reducer.d.ts +10 -0
  50. package/dist/reducer.d.ts.map +1 -1
  51. package/dist/reducer.js +19 -1
  52. package/dist/reducer.test.js +29 -1
  53. package/dist/session.svelte.d.ts +6 -0
  54. package/dist/session.svelte.d.ts.map +1 -1
  55. package/dist/session.svelte.js +15 -1
  56. package/dist/ui-frame-protocol.d.ts +2 -2
  57. package/dist/ui-frame-protocol.js +2 -2
  58. package/dist/ui-frame-relay.d.ts +16 -0
  59. package/dist/ui-frame-relay.d.ts.map +1 -0
  60. package/dist/ui-frame-relay.js +22 -0
  61. package/dist/ui-frame-relay.test.d.ts +2 -0
  62. package/dist/ui-frame-relay.test.d.ts.map +1 -0
  63. package/dist/ui-frame-relay.test.js +36 -0
  64. package/package.json +8 -5
@@ -63,42 +63,31 @@
63
63
  </script>
64
64
 
65
65
  <div class="sr-only" aria-live="polite" aria-atomic="true">{announcement}</div>
66
- <div class="agent-messages" bind:this={container} {onscroll} role="log" aria-label="Conversation">
66
+ <div
67
+ class="flex min-h-0 flex-1 flex-col overflow-y-auto py-2"
68
+ bind:this={container}
69
+ {onscroll}
70
+ role="log"
71
+ aria-label="Conversation"
72
+ >
67
73
  {#if messages.length === 0}
68
74
  {#if empty}{@render empty()}{/if}
69
75
  {:else}
70
- {#each messages as message, i (message.id)}
71
- <MessageItem
72
- {message}
73
- streaming={streaming && i === messages.length - 1 && message.role === 'assistant'}
74
- {sandboxOrigin}
75
- {fileUrl}
76
- {onnavigate}
77
- {onopenartifact}
78
- {onprompt}
79
- {ontool}
80
- {onnotify}
81
- onretry={i === messages.length - 1 ? onretry : undefined}
82
- />
83
- {/each}
76
+ <div class="mx-auto flex w-full max-w-3xl flex-col">
77
+ {#each messages as message, i (message.id)}
78
+ <MessageItem
79
+ {message}
80
+ streaming={streaming && i === messages.length - 1 && message.role === 'assistant'}
81
+ {sandboxOrigin}
82
+ {fileUrl}
83
+ {onnavigate}
84
+ {onopenartifact}
85
+ {onprompt}
86
+ {ontool}
87
+ {onnotify}
88
+ onretry={i === messages.length - 1 ? onretry : undefined}
89
+ />
90
+ {/each}
91
+ </div>
84
92
  {/if}
85
93
  </div>
86
-
87
- <style>
88
- .sr-only {
89
- position: absolute;
90
- width: 1px;
91
- height: 1px;
92
- overflow: hidden;
93
- clip: rect(0, 0, 0, 0);
94
- white-space: nowrap;
95
- }
96
- .agent-messages {
97
- flex: 1;
98
- min-height: 0;
99
- overflow-y: auto;
100
- padding: 0.5rem 0;
101
- display: flex;
102
- flex-direction: column;
103
- }
104
- </style>
@@ -1,23 +1,17 @@
1
1
  <script lang="ts">
2
+ import { Icon } from '@classic-homes/theme-svelte';
2
3
  import type { BackendMode } from '@classic-homes/agent-contracts';
3
4
  /** Mock must always be visible (AGENTS.md). Rendered whenever the stream reports `mock`. */
4
5
  let { mode }: { mode: BackendMode | undefined } = $props();
5
6
  </script>
6
7
 
7
8
  {#if mode === 'mock'}
8
- <div class="agent-mode-banner" role="status" data-testid="mode-banner">
9
+ <div
10
+ class="flex items-center gap-2 border-b border-border bg-warning px-3 py-1.5 text-xs font-semibold tracking-wide text-warning-foreground"
11
+ role="status"
12
+ data-testid="mode-banner"
13
+ >
14
+ <Icon name="alert-triangle" size={14} />
9
15
  MOCK BACKEND — simulated responses; no AI model or company system is contacted.
10
16
  </div>
11
17
  {/if}
12
-
13
- <style>
14
- .agent-mode-banner {
15
- padding: 0.375rem 0.75rem;
16
- font-size: 0.75rem;
17
- font-weight: 600;
18
- letter-spacing: 0.02em;
19
- color: hsl(var(--warning-foreground, 0 0% 10%));
20
- background: hsl(var(--warning, 45 93% 58%));
21
- border-bottom: 1px solid hsl(var(--border));
22
- }
23
- </style>
@@ -1 +1 @@
1
- {"version":3,"file":"ModeBanner.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ModeBanner.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAEjE,KAAK,gBAAgB,GAAI;IAAE,IAAI,EAAE,WAAW,GAAG,SAAS,CAAA;CAAE,CAAC;AAiB5D,QAAA,MAAM,UAAU,sDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"ModeBanner.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ModeBanner.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAEjE,KAAK,gBAAgB,GAAI;IAAE,IAAI,EAAE,WAAW,GAAG,SAAS,CAAA;CAAE,CAAC;AAiB5D,QAAA,MAAM,UAAU,sDAAwC,CAAC;AACzD,KAAK,UAAU,GAAG,UAAU,CAAC,OAAO,UAAU,CAAC,CAAC;AAChD,eAAe,UAAU,CAAC"}
@@ -0,0 +1,43 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { createFrameRelay, type FrameRelay } from '../ui-frame-relay.js';
4
+ import type { FrameAction } from '../ui-frame-protocol.js';
5
+
6
+ interface Props {
7
+ sandboxOrigin: string;
8
+ html: string;
9
+ uri: string;
10
+ title: string;
11
+ style?: string;
12
+ onaction?: (action: FrameAction) => void;
13
+ }
14
+ let { sandboxOrigin, html, uri, title, style = '', onaction }: Props = $props();
15
+ let frame: HTMLIFrameElement | null = $state(null);
16
+ let relay: FrameRelay | undefined = $state();
17
+
18
+ onMount(() => {
19
+ relay = createFrameRelay({
20
+ frame: () => frame,
21
+ origin: () => sandboxOrigin,
22
+ payload: () => ({ html, uri }),
23
+ onaction: (action) => onaction?.(action),
24
+ });
25
+ return () => relay?.destroy();
26
+ });
27
+ $effect(() => {
28
+ void html;
29
+ void uri;
30
+ relay?.send();
31
+ });
32
+ </script>
33
+
34
+ <iframe
35
+ bind:this={frame}
36
+ {title}
37
+ src={`${sandboxOrigin}/frame`}
38
+ sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms"
39
+ referrerpolicy="no-referrer"
40
+ allow=""
41
+ style={`display:block;width:100%;border:0;${style}`}
42
+ onload={() => relay?.send()}
43
+ ></iframe>
@@ -0,0 +1,13 @@
1
+ import type { FrameAction } from '../ui-frame-protocol.js';
2
+ interface Props {
3
+ sandboxOrigin: string;
4
+ html: string;
5
+ uri: string;
6
+ title: string;
7
+ style?: string;
8
+ onaction?: (action: FrameAction) => void;
9
+ }
10
+ declare const SandboxFrame: import("svelte").Component<Props, {}, "">;
11
+ type SandboxFrame = ReturnType<typeof SandboxFrame>;
12
+ export default SandboxFrame;
13
+ //# sourceMappingURL=SandboxFrame.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SandboxFrame.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/SandboxFrame.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAGzD,UAAU,KAAK;IACb,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;CAC1C;AA8BH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import { Icon } from '@classic-homes/theme-svelte';
2
+ import { cn, Icon } from '@classic-homes/theme-svelte';
3
3
  import type { DelegationState, ToolCallState } from '../reducer.js';
4
4
 
5
5
  let {
@@ -23,91 +23,40 @@
23
23
  </script>
24
24
 
25
25
  {#if tools.length > 0 || delegations.length > 0}
26
- <div class="agent-timeline">
26
+ <div class="mb-2 mt-1 text-xs text-muted-foreground">
27
27
  <button
28
28
  type="button"
29
- class="agent-timeline__toggle"
29
+ class="inline-flex items-center gap-1.5 rounded-sm py-0.5 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
30
30
  onclick={() => (expanded = !expanded)}
31
31
  aria-expanded={expanded}
32
32
  >
33
33
  <Icon name={expanded ? 'chevron-down' : 'chevron-right'} size={14} />
34
34
  <span>{label}</span>
35
- {#if streaming && running > 0}<span class="agent-timeline__dot" aria-hidden="true"
35
+ {#if streaming && running > 0}<span
36
+ class="size-2 rounded-full bg-primary motion-safe:animate-pulse"
37
+ aria-hidden="true"
36
38
  ></span>{/if}
37
39
  </button>
38
40
  {#if expanded}
39
- <ol class="agent-timeline__list">
41
+ <ol class="mt-1.5 grid gap-1 pl-4">
40
42
  {#each tools as tool (tool.callId)}
41
- <li
42
- class="agent-timeline__item"
43
- class:agent-timeline__item--error={tool.status === 'error' || tool.status === 'refused'}
44
- >
45
- <span class="agent-timeline__name"
46
- >{tool.server ? `${tool.server}/` : ''}{tool.name}</span
47
- >
48
- <span class="agent-timeline__agent">{tool.agent}</span>
49
- <span class="agent-timeline__status"
43
+ <li class="grid grid-cols-[auto_1fr_auto] gap-x-2">
44
+ <span class="font-mono">{tool.server ? `${tool.server}/` : ''}{tool.name}</span>
45
+ <span>{tool.agent}</span>
46
+ <span
47
+ class={cn(
48
+ (tool.status === 'error' || tool.status === 'refused') && 'text-destructive',
49
+ )}
50
50
  >{tool.status ?? 'running'}{tool.durationMs !== undefined
51
51
  ? ` · ${tool.durationMs} ms`
52
52
  : ''}</span
53
53
  >
54
- {#if tool.summary}<div class="agent-timeline__summary">{tool.summary}</div>{/if}
54
+ {#if tool.summary}<div class="col-span-full whitespace-pre-wrap opacity-80">
55
+ {tool.summary}
56
+ </div>{/if}
55
57
  </li>
56
58
  {/each}
57
59
  </ol>
58
60
  {/if}
59
61
  </div>
60
62
  {/if}
61
-
62
- <style>
63
- .agent-timeline {
64
- margin: 0.25rem 0 0.5rem;
65
- font-size: 0.75rem;
66
- color: hsl(var(--muted-foreground));
67
- }
68
- .agent-timeline__toggle {
69
- display: inline-flex;
70
- align-items: center;
71
- gap: 0.375rem;
72
- background: none;
73
- border: 0;
74
- padding: 0.125rem 0;
75
- color: inherit;
76
- cursor: pointer;
77
- font: inherit;
78
- }
79
- .agent-timeline__dot {
80
- width: 0.5rem;
81
- height: 0.5rem;
82
- border-radius: 9999px;
83
- background: hsl(var(--primary));
84
- animation: agent-pulse 1.2s ease-in-out infinite;
85
- }
86
- @keyframes agent-pulse {
87
- 50% {
88
- opacity: 0.3;
89
- }
90
- }
91
- .agent-timeline__list {
92
- margin: 0.375rem 0 0;
93
- padding-left: 1rem;
94
- display: grid;
95
- gap: 0.25rem;
96
- }
97
- .agent-timeline__item {
98
- display: grid;
99
- grid-template-columns: auto 1fr auto;
100
- gap: 0 0.5rem;
101
- }
102
- .agent-timeline__item--error .agent-timeline__status {
103
- color: hsl(var(--destructive));
104
- }
105
- .agent-timeline__name {
106
- font-family: ui-monospace, monospace;
107
- }
108
- .agent-timeline__summary {
109
- grid-column: 1 / -1;
110
- white-space: pre-wrap;
111
- opacity: 0.8;
112
- }
113
- </style>
@@ -1 +1 @@
1
- {"version":3,"file":"ToolTimeline.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ToolTimeline.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnE,KAAK,gBAAgB,GAAI;IAAE,KAAK,EAAE,aAAa,EAAE,CAAC;IAAC,WAAW,EAAE,eAAe,EAAE,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAqD1G,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"ToolTimeline.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ToolTimeline.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnE,KAAK,gBAAgB,GAAI;IAAE,KAAK,EAAE,aAAa,EAAE,CAAC;IAAC,WAAW,EAAE,eAAe,EAAE,CAAC;IAAC,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAuD1G,QAAA,MAAM,YAAY,sDAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -1,15 +1,7 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
2
+ import SandboxFrame from './SandboxFrame.svelte';
3
3
  import type { UiResourceState } from '../reducer.js';
4
- import {
5
- FRAME_TARGET_ORIGIN,
6
- MIN_FRAME_HEIGHT,
7
- UI_FRAME_RENDER,
8
- composerTextFor,
9
- isFromFrame,
10
- parseFrameMessage,
11
- type FrameAction,
12
- } from '../ui-frame-protocol.js';
4
+ import { MIN_FRAME_HEIGHT, composerTextFor, type FrameAction } from '../ui-frame-protocol.js';
13
5
 
14
6
  interface Props {
15
7
  resource: UiResourceState;
@@ -21,22 +13,11 @@
21
13
  }
22
14
  let { resource, sandboxOrigin, onprompt, ontool, onnotify }: Props = $props();
23
15
 
24
- let frame: HTMLIFrameElement | null = $state(null);
25
16
  let height = $state(MIN_FRAME_HEIGHT);
26
17
  const initialHeight = $derived(resource.preferredFrameSize?.[1] ?? `${MIN_FRAME_HEIGHT}px`);
27
18
 
28
- function send(): void {
29
- frame?.contentWindow?.postMessage(
30
- { type: UI_FRAME_RENDER, html: resource.html, uri: resource.uri },
31
- FRAME_TARGET_ORIGIN,
32
- );
33
- }
34
-
35
19
  function handle(action: FrameAction): void {
36
20
  switch (action.type) {
37
- case 'ready':
38
- send();
39
- break;
40
21
  case 'size':
41
22
  height = action.height;
42
23
  break;
@@ -55,42 +36,15 @@
55
36
  break;
56
37
  }
57
38
  }
58
-
59
- onMount(() => {
60
- const listener = (event: MessageEvent) => {
61
- if (!isFromFrame(event, frame, sandboxOrigin)) return;
62
- const action = parseFrameMessage(event.data);
63
- if (action) handle(action);
64
- };
65
- window.addEventListener('message', listener);
66
- return () => window.removeEventListener('message', listener);
67
- });
68
39
  </script>
69
40
 
70
- <div class="agent-ui-frame">
71
- <iframe
72
- bind:this={frame}
41
+ <div class="my-2 overflow-hidden rounded-lg border border-border bg-card">
42
+ <SandboxFrame
43
+ {sandboxOrigin}
44
+ html={resource.html}
45
+ uri={resource.uri}
73
46
  title={resource.uri}
74
- src={`${sandboxOrigin}/frame`}
75
- sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms"
76
- referrerpolicy="no-referrer"
77
- allow=""
78
47
  style={`height:${height > MIN_FRAME_HEIGHT ? `${height}px` : initialHeight}`}
79
- onload={send}
80
- ></iframe>
48
+ onaction={handle}
49
+ />
81
50
  </div>
82
-
83
- <style>
84
- .agent-ui-frame {
85
- margin: 0.5rem 0;
86
- border: 1px solid hsl(var(--border));
87
- border-radius: 0.5rem;
88
- overflow: hidden;
89
- background: hsl(var(--card));
90
- }
91
- iframe {
92
- display: block;
93
- width: 100%;
94
- border: 0;
95
- }
96
- </style>
@@ -1 +1 @@
1
- {"version":3,"file":"UiFrame.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/UiFrame.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAYnD,UAAU,KAAK;IACb,QAAQ,EAAE,eAAe,CAAC;IAC1B,gGAAgG;IAChG,aAAa,EAAE,MAAM,CAAC;IACtB,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;CACtC;AA8DH,QAAA,MAAM,OAAO,2CAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
1
+ {"version":3,"file":"UiFrame.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/UiFrame.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAInD,UAAU,KAAK;IACb,QAAQ,EAAE,eAAe,CAAC;IAC1B,gGAAgG;IAChG,aAAa,EAAE,MAAM,CAAC;IACtB,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;CACtC;AAuCH,QAAA,MAAM,OAAO,2CAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=conversation-chrome.svelte.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conversation-chrome.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/conversation-chrome.svelte.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,127 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { cleanup, render, screen, waitFor } from '@testing-library/svelte';
3
+ import userEvent from '@testing-library/user-event';
4
+ import ChatHeader from './ChatHeader.svelte';
5
+ import ConversationSidebar from './ConversationSidebar.svelte';
6
+ afterEach(() => {
7
+ cleanup();
8
+ // bits-ui locks body pointer events while a menu is open; an unmount mid-open leaves it set.
9
+ document.body.style.pointerEvents = '';
10
+ vi.useRealTimers();
11
+ });
12
+ function summary(id, title, extra = {}) {
13
+ return {
14
+ conversationId: id,
15
+ title,
16
+ agent: 'classic-agent',
17
+ createdAt: new Date().toISOString(),
18
+ updatedAt: new Date().toISOString(),
19
+ ...extra,
20
+ };
21
+ }
22
+ const conversations = [summary('c1', 'Lot 12 budget'), summary('c2', 'Warranty letters')];
23
+ describe('ConversationSidebar', () => {
24
+ it('debounces search and passes the trimmed query', async () => {
25
+ vi.useFakeTimers({ shouldAdvanceTime: true });
26
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
27
+ const onsearch = vi.fn();
28
+ render(ConversationSidebar, { props: { conversations, onsearch, searchDelay: 200 } });
29
+ await user.type(screen.getByRole('searchbox', { name: 'Search conversations' }), ' lot ');
30
+ expect(onsearch).not.toHaveBeenCalled();
31
+ await vi.advanceTimersByTimeAsync(200);
32
+ expect(onsearch).toHaveBeenCalledTimes(1);
33
+ expect(onsearch).toHaveBeenCalledWith('lot');
34
+ });
35
+ it('shows skeleton rows while the first page loads, then an empty state', async () => {
36
+ const { rerender } = render(ConversationSidebar, {
37
+ props: { conversations: [], loading: true },
38
+ });
39
+ expect(screen.getByLabelText('Loading conversations')).toBeTruthy();
40
+ await rerender({ conversations: [], loading: false });
41
+ expect(screen.getByText('No conversations yet')).toBeTruthy();
42
+ });
43
+ it('marks the selected row and a running conversation', () => {
44
+ render(ConversationSidebar, {
45
+ props: {
46
+ conversations: [summary('c1', 'Lot 12 budget', { activeRequestId: 'r1' })],
47
+ selectedId: 'c1',
48
+ },
49
+ });
50
+ const row = screen.getByRole('button', { name: /Lot 12 budget/ });
51
+ expect(row.getAttribute('aria-current')).toBe('page');
52
+ expect(screen.getByText('Answering')).toBeTruthy();
53
+ });
54
+ it('renames from the row menu and keeps the dialog open on failure', async () => {
55
+ const user = userEvent.setup();
56
+ const onrename = vi
57
+ .fn()
58
+ .mockRejectedValueOnce(new Error('Title is taken'))
59
+ .mockResolvedValueOnce(undefined);
60
+ render(ConversationSidebar, { props: { conversations, onrename } });
61
+ await user.click(screen.getByRole('button', { name: 'Options for Lot 12 budget' }));
62
+ await user.click(await screen.findByRole('menuitem', { name: /Rename/ }));
63
+ const field = await screen.findByRole('textbox', { name: 'Conversation title' });
64
+ await user.clear(field);
65
+ await user.type(field, 'Lot 12 final');
66
+ await user.click(screen.getByRole('button', { name: 'Save' }));
67
+ expect(onrename).toHaveBeenCalledWith('c1', 'Lot 12 final');
68
+ expect((await screen.findByRole('alert')).textContent).toContain('Title is taken');
69
+ await user.click(screen.getByRole('button', { name: 'Save' }));
70
+ await waitFor(() => expect(screen.queryByRole('textbox', { name: 'Conversation title' })).toBeNull());
71
+ expect(onrename).toHaveBeenCalledTimes(2);
72
+ });
73
+ it('returns focus to the row menu after renaming', async () => {
74
+ const user = userEvent.setup();
75
+ render(ConversationSidebar, { props: { conversations, onrename: vi.fn() } });
76
+ const trigger = screen.getByRole('button', { name: 'Options for Lot 12 budget' });
77
+ await user.click(trigger);
78
+ await user.click(await screen.findByRole('menuitem', { name: /Rename/ }));
79
+ await user.click(await screen.findByRole('button', { name: 'Cancel' }));
80
+ await waitFor(() => expect(document.activeElement).toBe(trigger));
81
+ });
82
+ it('deletes only after confirming and reports a failure', async () => {
83
+ const user = userEvent.setup();
84
+ const ondelete = vi.fn().mockRejectedValue(new Error('Network down'));
85
+ const onerror = vi.fn();
86
+ render(ConversationSidebar, { props: { conversations, ondelete, onerror } });
87
+ await user.click(screen.getByRole('button', { name: 'Options for Warranty letters' }));
88
+ await user.click(await screen.findByRole('menuitem', { name: /Delete/ }));
89
+ expect(ondelete).not.toHaveBeenCalled();
90
+ expect(await screen.findByText('Delete this conversation?')).toBeTruthy();
91
+ await user.click(screen.getByRole('button', { name: 'Delete' }));
92
+ await waitFor(() => expect(onerror).toHaveBeenCalledWith('Network down'));
93
+ expect(ondelete).toHaveBeenCalledWith('c2');
94
+ });
95
+ });
96
+ describe('ChatHeader', () => {
97
+ it('shows the title, falls back to "New chat" and hides actions without handlers', () => {
98
+ render(ChatHeader, { props: { title: ' ' } });
99
+ expect(screen.getByRole('heading', { name: 'New chat' })).toBeTruthy();
100
+ expect(screen.queryByRole('button', { name: 'Conversation options' })).toBeNull();
101
+ expect(screen.queryByRole('button', { name: 'Show conversations' })).toBeNull();
102
+ });
103
+ const handlers = () => ({ title: 'Lot 12 budget', onrename: vi.fn(), ondelete: vi.fn() });
104
+ it('offers no rename or delete before the conversation is saved', async () => {
105
+ const user = userEvent.setup();
106
+ render(ChatHeader, { props: { ...handlers(), onexpand: vi.fn() } });
107
+ await user.click(screen.getByRole('button', { name: 'Conversation options' }));
108
+ expect(await screen.findByRole('menuitem', { name: /Open full page/ })).toBeTruthy();
109
+ expect(screen.queryByRole('menuitem', { name: /Rename/ })).toBeNull();
110
+ expect(screen.queryByRole('menuitem', { name: /Delete/ })).toBeNull();
111
+ });
112
+ it('offers rename and delete for a saved conversation', async () => {
113
+ const user = userEvent.setup();
114
+ render(ChatHeader, { props: { ...handlers(), conversationId: 'c1' } });
115
+ await user.click(screen.getByRole('button', { name: 'Conversation options' }));
116
+ expect(await screen.findByRole('menuitem', { name: /Rename/ })).toBeTruthy();
117
+ expect(screen.getByRole('menuitem', { name: /Delete/ })).toBeTruthy();
118
+ });
119
+ it('opens the conversation rail', async () => {
120
+ const user = userEvent.setup();
121
+ const onopenrail = vi.fn();
122
+ render(ChatHeader, { props: { title: 'x', onopenrail, agentLabel: 'Classic Agent' } });
123
+ expect(screen.getByText('Classic Agent')).toBeTruthy();
124
+ await user.click(screen.getByRole('button', { name: 'Show conversations' }));
125
+ expect(onopenrail).toHaveBeenCalledOnce();
126
+ });
127
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=markdown-prose.svelte.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown-prose.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/markdown-prose.svelte.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { cleanup, render } from '@testing-library/svelte';
3
+ import { parseMarkdown } from '../markdown.js';
4
+ import { errorTitle } from '../reducer.js';
5
+ import Markdown from './Markdown.svelte';
6
+ afterEach(cleanup);
7
+ describe('Markdown', () => {
8
+ // The Classic App's cascade-layers polyfill outranks plain component CSS, so the prose rules
9
+ // must be utilities on the container (classic-agent visual check, 2026-11-03).
10
+ it('styles prose with utility classes, not a stylesheet', () => {
11
+ const { container } = render(Markdown, {
12
+ props: { html: parseMarkdown('- one\n- two'), class: 'text-sm' },
13
+ });
14
+ const root = container.firstElementChild;
15
+ expect(root.className).toContain('[&_ul]:list-disc');
16
+ expect(root.className).toContain('[&_td]:border');
17
+ expect(root.className).toContain('text-sm');
18
+ expect(root.querySelectorAll('li')).toHaveLength(2);
19
+ expect(document.querySelectorAll('style')).toHaveLength(0);
20
+ });
21
+ });
22
+ describe('errorTitle', () => {
23
+ it('falls back for a code this client does not know', () => {
24
+ expect(errorTitle('upstream')).toBe('Something went wrong');
25
+ });
26
+ });
@@ -0,0 +1,11 @@
1
+ /** What a keydown in the composer textarea should do. Pure so it can be tested without a DOM. */
2
+ export type ComposerKeyAction = 'send' | 'cancel' | null;
3
+ export interface ComposerKey {
4
+ key: string;
5
+ shiftKey?: boolean;
6
+ isComposing?: boolean;
7
+ /** 229 is what browsers report for keys consumed by an input method editor. */
8
+ keyCode?: number;
9
+ }
10
+ export declare function composerKeyAction(event: ComposerKey, streaming: boolean): ComposerKeyAction;
11
+ //# sourceMappingURL=composer-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer-keys.d.ts","sourceRoot":"","sources":["../src/lib/composer-keys.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,IAAI,CAAC;AAEzD,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO,GAAG,iBAAiB,CAM3F"}
@@ -0,0 +1,10 @@
1
+ export function composerKeyAction(event, streaming) {
2
+ // Enter that confirms an IME candidate (Japanese, Chinese, Korean input) must not send.
3
+ if (event.isComposing || event.keyCode === 229)
4
+ return null;
5
+ if (event.key === 'Escape')
6
+ return streaming ? 'cancel' : null;
7
+ if (event.key === 'Enter' && !event.shiftKey)
8
+ return 'send';
9
+ return null;
10
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=composer-keys.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer-keys.test.d.ts","sourceRoot":"","sources":["../src/lib/composer-keys.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,16 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { composerKeyAction } from './composer-keys.js';
3
+ describe('composerKeyAction', () => {
4
+ it('sends on Enter and keeps Shift+Enter for a newline', () => {
5
+ expect(composerKeyAction({ key: 'Enter' }, false)).toBe('send');
6
+ expect(composerKeyAction({ key: 'Enter', shiftKey: true }, false)).toBeNull();
7
+ });
8
+ it('ignores Enter that confirms an IME candidate', () => {
9
+ expect(composerKeyAction({ key: 'Enter', isComposing: true }, false)).toBeNull();
10
+ expect(composerKeyAction({ key: 'Enter', keyCode: 229 }, false)).toBeNull();
11
+ });
12
+ it('stops a running reply on Escape only while streaming', () => {
13
+ expect(composerKeyAction({ key: 'Escape' }, true)).toBe('cancel');
14
+ expect(composerKeyAction({ key: 'Escape' }, false)).toBeNull();
15
+ });
16
+ });
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './composer-keys.js';
1
2
  export * from './markdown.js';
2
3
  export * from './reducer.js';
3
4
  export * from './session.svelte.js';
@@ -11,4 +12,5 @@ export { default as MessageList } from './components/MessageList.svelte';
11
12
  export { default as ModeBanner } from './components/ModeBanner.svelte';
12
13
  export { default as ToolTimeline } from './components/ToolTimeline.svelte';
13
14
  export { default as UiFrame } from './components/UiFrame.svelte';
15
+ export { default as ChatHeader } from './components/ChatHeader.svelte';
14
16
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/lib/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,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"}
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './composer-keys.js';
1
2
  export * from './markdown.js';
2
3
  export * from './reducer.js';
3
4
  export * from './session.svelte.js';
@@ -11,3 +12,4 @@ export { default as MessageList } from './components/MessageList.svelte';
11
12
  export { default as ModeBanner } from './components/ModeBanner.svelte';
12
13
  export { default as ToolTimeline } from './components/ToolTimeline.svelte';
13
14
  export { default as UiFrame } from './components/UiFrame.svelte';
15
+ export { default as ChatHeader } from './components/ChatHeader.svelte';