@classic-homes/agent-chat 0.2.1 → 0.3.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 (41) hide show
  1. package/README.md +14 -2
  2. package/dist/components/AgentChat.svelte +1 -0
  3. package/dist/components/ArtifactChip.svelte +45 -0
  4. package/dist/components/ArtifactChip.svelte.d.ts +14 -0
  5. package/dist/components/ArtifactChip.svelte.d.ts.map +1 -0
  6. package/dist/components/AssistantMessage.svelte +179 -0
  7. package/dist/components/AssistantMessage.svelte.d.ts +25 -0
  8. package/dist/components/AssistantMessage.svelte.d.ts.map +1 -0
  9. package/dist/components/Markdown.svelte +87 -5
  10. package/dist/components/Markdown.svelte.d.ts +9 -1
  11. package/dist/components/Markdown.svelte.d.ts.map +1 -1
  12. package/dist/components/MessageActions.svelte +56 -0
  13. package/dist/components/MessageActions.svelte.d.ts +17 -0
  14. package/dist/components/MessageActions.svelte.d.ts.map +1 -0
  15. package/dist/components/MessageFiles.svelte +83 -0
  16. package/dist/components/MessageFiles.svelte.d.ts +14 -0
  17. package/dist/components/MessageFiles.svelte.d.ts.map +1 -0
  18. package/dist/components/MessageItem.svelte +11 -144
  19. package/dist/components/MessageItem.svelte.d.ts +3 -0
  20. package/dist/components/MessageItem.svelte.d.ts.map +1 -1
  21. package/dist/components/MessageList.svelte +99 -30
  22. package/dist/components/MessageList.svelte.d.ts +1 -0
  23. package/dist/components/MessageList.svelte.d.ts.map +1 -1
  24. package/dist/components/ProposalCard.svelte +39 -0
  25. package/dist/components/ProposalCard.svelte.d.ts +13 -0
  26. package/dist/components/ProposalCard.svelte.d.ts.map +1 -0
  27. package/dist/components/UserMessage.svelte +46 -0
  28. package/dist/components/UserMessage.svelte.d.ts +10 -0
  29. package/dist/components/UserMessage.svelte.d.ts.map +1 -0
  30. package/dist/components/messages.svelte.test.d.ts +2 -0
  31. package/dist/components/messages.svelte.test.d.ts.map +1 -0
  32. package/dist/components/messages.svelte.test.js +202 -0
  33. package/dist/index.d.ts +7 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +7 -0
  36. package/dist/markdown.d.ts.map +1 -1
  37. package/dist/markdown.js +12 -4
  38. package/dist/message-format.d.ts +30 -0
  39. package/dist/message-format.d.ts.map +1 -0
  40. package/dist/message-format.js +118 -0
  41. package/package.json +3 -3
@@ -1,15 +1,16 @@
1
+ <!--
2
+ @component
3
+ One transcript entry: a `UserMessage` or an `AssistantMessage`.
4
+ -->
1
5
  <script lang="ts">
2
- import { Button, Icon } from '@classic-homes/theme-svelte';
3
- import Markdown from './Markdown.svelte';
4
6
  import type { ChatMessage } from '../reducer.js';
5
- import { errorTitle } from '../reducer.js';
6
- import { parseMarkdown } from '../markdown.js';
7
- import ToolTimeline from './ToolTimeline.svelte';
8
- import UiFrame from './UiFrame.svelte';
7
+ import AssistantMessage from './AssistantMessage.svelte';
8
+ import UserMessage from './UserMessage.svelte';
9
9
 
10
10
  interface Props {
11
11
  message: ChatMessage;
12
12
  streaming?: boolean;
13
+ last?: boolean;
13
14
  sandboxOrigin: string;
14
15
  fileUrl: (fileId: string) => string;
15
16
  onnavigate?: (href: string) => void;
@@ -18,147 +19,13 @@
18
19
  ontool?: (toolName: string, params?: Record<string, unknown>) => void;
19
20
  onnotify?: (message: string) => void;
20
21
  onretry?: () => void;
22
+ oncontinue?: () => void;
21
23
  }
22
- let {
23
- message,
24
- streaming = false,
25
- sandboxOrigin,
26
- fileUrl,
27
- onnavigate,
28
- onopenartifact,
29
- onprompt,
30
- ontool,
31
- onnotify,
32
- onretry,
33
- }: Props = $props();
34
-
35
- // Re-parsing markdown on every token is the dominant streaming cost; throttle it.
36
- const PARSE_THROTTLE_MS = 120;
37
- let html = $state('');
38
- let lastParse = 0;
39
- let timer: ReturnType<typeof setTimeout> | null = null;
40
- $effect(() => {
41
- if (message.role !== 'assistant') return;
42
- const text = message.text;
43
- const now = Date.now();
44
- if (!streaming || now - lastParse >= PARSE_THROTTLE_MS) {
45
- if (timer) clearTimeout(timer);
46
- timer = null;
47
- lastParse = now;
48
- html = parseMarkdown(text);
49
- } else if (!timer) {
50
- timer = setTimeout(
51
- () => {
52
- timer = null;
53
- lastParse = Date.now();
54
- html = parseMarkdown(message.role === 'assistant' ? message.text : '');
55
- },
56
- PARSE_THROTTLE_MS - (now - lastParse),
57
- );
58
- }
59
- });
60
- $effect(() => () => {
61
- if (timer) clearTimeout(timer);
62
- });
63
-
64
- function handleClick(event: MouseEvent): void {
65
- if (!onnavigate) return;
66
- const anchor = (event.target as HTMLElement | null)?.closest?.('a.agent-internal-link');
67
- if (!(anchor instanceof HTMLAnchorElement)) return;
68
- event.preventDefault();
69
- onnavigate(anchor.getAttribute('href') ?? anchor.href);
70
- }
71
-
72
- function formatBytes(n: number): string {
73
- return n < 1024
74
- ? `${n} B`
75
- : n < 1048576
76
- ? `${(n / 1024).toFixed(0)} KB`
77
- : `${(n / 1048576).toFixed(1)} MB`;
78
- }
24
+ let { message, last = false, ...rest }: Props = $props();
79
25
  </script>
80
26
 
81
27
  {#if message.role === 'user'}
82
- <div class="ml-auto flex max-w-[85%] flex-col items-end gap-1 px-4 py-2">
83
- {#if message.contextLabel}<div class="text-xs text-muted-foreground">
84
- Context: {message.contextLabel}
85
- </div>{/if}
86
- <div
87
- class="whitespace-pre-wrap rounded-2xl rounded-br-md bg-primary px-3.5 py-2 text-primary-foreground [overflow-wrap:anywhere]"
88
- >
89
- {message.text}
90
- </div>
91
- {#if message.attachments.length}
92
- <ul class="flex flex-wrap justify-end gap-2 text-xs text-muted-foreground">
93
- {#each message.attachments as file (file.fileId)}<li class="inline-flex items-center gap-1">
94
- <Icon name="file" size={12} />
95
- {file.name}
96
- </li>{/each}
97
- </ul>
98
- {/if}
99
- </div>
28
+ <UserMessage {message} {last} />
100
29
  {:else}
101
- <div class="min-w-0 px-4 py-2" data-status={message.status}>
102
- <ToolTimeline tools={message.tools} delegations={message.delegations} {streaming} />
103
- {#if html}<Markdown {html} onclick={handleClick} />{/if}
104
- {#if streaming && !message.text}<span class="italic text-muted-foreground">Thinking…</span>{/if}
105
- {#if message.artifacts.length}
106
- <div class="my-2 flex flex-wrap gap-1.5">
107
- {#each message.artifacts as a (a.artifactId)}
108
- <Button variant="outline" size="sm" onclick={() => onopenartifact?.(a.artifactId)}>
109
- <Icon name="file-text" size={14} />
110
- {a.title ?? a.artifactId}{a.open ? '…' : ''}
111
- </Button>
112
- {/each}
113
- </div>
114
- {/if}
115
- {#each message.ui as resource (resource.callId + resource.uri)}
116
- <UiFrame {resource} {sandboxOrigin} {onprompt} {ontool} {onnotify} />
117
- {/each}
118
- {#if message.files.length}
119
- <ul class="mt-1 flex flex-wrap gap-2 text-sm">
120
- {#each message.files as file (file.fileId)}
121
- <li>
122
- {#if file.mimeType.startsWith('image/')}
123
- <img
124
- class="mb-1 block max-h-96 max-w-full rounded-lg"
125
- src={fileUrl(file.fileId)}
126
- alt={file.name}
127
- loading="lazy"
128
- />
129
- {/if}
130
- <a
131
- class="inline-flex items-center gap-1 text-primary underline-offset-2 hover:underline"
132
- href={fileUrl(file.fileId)}
133
- download={file.name}
134
- ><Icon name="download" size={14} /> {file.name} ({formatBytes(file.bytes)})</a
135
- >
136
- </li>
137
- {/each}
138
- </ul>
139
- {/if}
140
- {#if message.proposals.length}
141
- <div class="mt-1 text-xs text-muted-foreground">
142
- Proposed {message.proposals.length} document edit{message.proposals.length === 1
143
- ? ''
144
- : 's'}; apply them from the Excel or Word add-in.
145
- </div>
146
- {/if}
147
- {#if message.error}
148
- <div
149
- class="mt-2 rounded-lg bg-destructive/10 px-3 py-2 text-sm text-destructive"
150
- role="alert"
151
- >
152
- <strong>{errorTitle(message.error.code)}.</strong>
153
- {message.error.message}
154
- {#if message.error.retryable && onretry}<Button variant="link" size="sm" onclick={onretry}
155
- >Try again</Button
156
- >{/if}
157
- </div>
158
- {:else if message.status === 'cancelled'}
159
- <div class="mt-1 text-xs text-muted-foreground">Stopped.</div>
160
- {:else if message.status === 'incomplete'}
161
- <div class="mt-1 text-xs text-muted-foreground">The answer was cut short.</div>
162
- {/if}
163
- </div>
30
+ <AssistantMessage {message} {last} {...rest} />
164
31
  {/if}
@@ -2,6 +2,7 @@ import type { ChatMessage } from '../reducer.js';
2
2
  interface Props {
3
3
  message: ChatMessage;
4
4
  streaming?: boolean;
5
+ last?: boolean;
5
6
  sandboxOrigin: string;
6
7
  fileUrl: (fileId: string) => string;
7
8
  onnavigate?: (href: string) => void;
@@ -10,7 +11,9 @@ interface Props {
10
11
  ontool?: (toolName: string, params?: Record<string, unknown>) => void;
11
12
  onnotify?: (message: string) => void;
12
13
  onretry?: () => void;
14
+ oncontinue?: () => void;
13
15
  }
16
+ /** One transcript entry: a `UserMessage` or an `AssistantMessage`. */
14
17
  declare const MessageItem: import("svelte").Component<Props, {}, "">;
15
18
  type MessageItem = ReturnType<typeof MessageItem>;
16
19
  export default MessageItem;
@@ -1 +1 @@
1
- {"version":3,"file":"MessageItem.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageItem.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAO/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,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;CACtB;AA2IH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"MessageItem.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageItem.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,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,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;CACzB;AAkBH,sEAAsE;AACtE,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { tick } from 'svelte';
3
+ import { Button, Icon } from '@classic-homes/theme-svelte';
3
4
  import type { ChatMessage } from '../reducer.js';
5
+ import { daySeparators, formatDay } from '../message-format.js';
4
6
  import MessageItem from './MessageItem.svelte';
5
7
 
6
8
  interface Props {
@@ -14,6 +16,7 @@
14
16
  ontool?: (toolName: string, params?: Record<string, unknown>) => void;
15
17
  onnotify?: (message: string) => void;
16
18
  onretry?: () => void;
19
+ oncontinue?: () => void;
17
20
  empty?: import('svelte').Snippet;
18
21
  }
19
22
  let {
@@ -27,33 +30,70 @@
27
30
  ontool,
28
31
  onnotify,
29
32
  onretry,
33
+ oncontinue,
30
34
  empty,
31
35
  }: Props = $props();
32
36
 
33
37
  let container: HTMLDivElement | undefined = $state();
34
- let follow = true;
38
+ /** Pinned to the bottom: new content scrolls into view. Off once the reader scrolls up. */
39
+ let follow = $state(true);
40
+ /** Content arrived while the reader was scrolled up. */
41
+ let unseen = $state(false);
35
42
  const FOLLOW_THRESHOLD_PX = 80;
43
+ function atBottom(): boolean {
44
+ if (!container) return true;
45
+ return (
46
+ container.scrollTop + container.clientHeight >= container.scrollHeight - FOLLOW_THRESHOLD_PX
47
+ );
48
+ }
36
49
  function onscroll(): void {
50
+ follow = atBottom();
51
+ if (follow) unseen = false;
52
+ }
53
+ function scrollToBottom(smooth = false): void {
37
54
  if (!container) return;
38
- follow =
39
- container.scrollTop + container.clientHeight >= container.scrollHeight - FOLLOW_THRESHOLD_PX;
55
+ const reduce =
56
+ typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
57
+ container.scrollTo({
58
+ top: container.scrollHeight,
59
+ behavior: smooth && !reduce ? 'smooth' : 'auto',
60
+ });
61
+ }
62
+ function jumpToLatest(): void {
63
+ follow = true;
64
+ unseen = false;
65
+ scrollToBottom(true);
40
66
  }
67
+
41
68
  const count = $derived(messages.length);
42
69
  const lastRole = $derived(messages[messages.length - 1]?.role);
43
70
  let prevCount = 0;
44
71
  $effect(() => {
45
72
  void messages;
46
- if (streaming && follow && container) container.scrollTop = container.scrollHeight;
73
+ // Also when streaming stops: the turn's footer (outcome, actions) appears then.
74
+ void streaming;
75
+ if (!container) return;
76
+ if (follow) container.scrollTop = container.scrollHeight;
77
+ else if (streaming) unseen = true;
47
78
  });
48
79
  $effect(() => {
49
80
  void count;
50
81
  void lastRole;
51
- if (count > prevCount && lastRole === 'user') follow = true;
82
+ // Sending always brings the reader back to the newest turn.
83
+ if (count > prevCount && lastRole === 'user') {
84
+ follow = true;
85
+ unseen = false;
86
+ }
52
87
  prevCount = count;
53
88
  void tick().then(() => {
54
89
  if (container && follow) container.scrollTop = container.scrollHeight;
55
90
  });
56
91
  });
92
+
93
+ const separators = $derived(daySeparators(messages));
94
+ // Only show day labels when the transcript spans more than one day.
95
+ const multiDay = $derived(separators.size > 1);
96
+
57
97
  // Announce the finished answer once, not every token.
58
98
  const announcement = $derived.by(() => {
59
99
  if (streaming) return '';
@@ -63,31 +103,60 @@
63
103
  </script>
64
104
 
65
105
  <div class="sr-only" aria-live="polite" aria-atomic="true">{announcement}</div>
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
- >
73
- {#if messages.length === 0}
74
- {#if empty}{@render empty()}{/if}
75
- {:else}
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}
106
+ <div class="relative flex min-h-0 flex-1 flex-col">
107
+ <div
108
+ class="flex min-h-0 flex-1 flex-col overflow-y-auto py-2"
109
+ bind:this={container}
110
+ {onscroll}
111
+ role="log"
112
+ aria-label="Conversation"
113
+ >
114
+ {#if messages.length === 0}
115
+ {#if empty}{@render empty()}{/if}
116
+ {:else}
117
+ <div class="mx-auto flex w-full max-w-3xl flex-col">
118
+ {#each messages as message, i (message.id)}
119
+ {#if multiDay && separators.has(i)}
120
+ <div
121
+ class="flex items-center gap-3 px-4 py-3 text-xs font-medium text-muted-foreground"
122
+ role="separator"
123
+ aria-label={formatDay(message.createdAt)}
124
+ >
125
+ <span class="h-px flex-1 bg-border"></span>
126
+ {formatDay(message.createdAt)}
127
+ <span class="h-px flex-1 bg-border"></span>
128
+ </div>
129
+ {/if}
130
+ <MessageItem
131
+ {message}
132
+ last={i === messages.length - 1}
133
+ streaming={streaming && i === messages.length - 1 && message.role === 'assistant'}
134
+ {sandboxOrigin}
135
+ {fileUrl}
136
+ {onnavigate}
137
+ {onopenartifact}
138
+ {onprompt}
139
+ {ontool}
140
+ {onnotify}
141
+ onretry={i === messages.length - 1 ? onretry : undefined}
142
+ oncontinue={i === messages.length - 1 && !streaming ? oncontinue : undefined}
143
+ />
144
+ {/each}
145
+ </div>
146
+ {/if}
147
+ </div>
148
+ {#if !follow && messages.length > 0}
149
+ <div class="pointer-events-none absolute inset-x-0 bottom-3 flex justify-center">
150
+ <Button
151
+ variant={unseen ? 'default' : 'outline'}
152
+ size="sm"
153
+ class="pointer-events-auto rounded-full shadow-md"
154
+ onclick={jumpToLatest}
155
+ aria-label={unseen ? 'New messages; jump to the latest' : 'Jump to the latest message'}
156
+ >
157
+ <Icon name="arrow-down" size={14} />
158
+ {unseen ? 'New messages' : 'Latest'}
159
+ </Button>
91
160
  </div>
92
161
  {/if}
93
162
  </div>
@@ -10,6 +10,7 @@ interface Props {
10
10
  ontool?: (toolName: string, params?: Record<string, unknown>) => void;
11
11
  onnotify?: (message: string) => void;
12
12
  onretry?: () => void;
13
+ oncontinue?: () => void;
13
14
  empty?: import('svelte').Snippet;
14
15
  }
15
16
  declare const MessageList: import("svelte").Component<Props, {}, "">;
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageList.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAI/C,UAAU,KAAK;IACb,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,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,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CAClC;AAmEH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"MessageList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MessageList.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,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,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,KAAK,CAAC,EAAE,OAAO,QAAQ,EAAE,OAAO,CAAC;CAClC;AA2HH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,39 @@
1
+ <!--
2
+ @component
3
+ Read-only view of a document edit the agent proposed. The chat never applies edits: they are
4
+ reviewed and applied from the Excel or Word add-in, which re-reads the target first.
5
+ -->
6
+ <script lang="ts">
7
+ import type { ProposedOperation } from '@classic-homes/agent-contracts';
8
+ import { cn, Icon } from '@classic-homes/theme-svelte';
9
+ import { describeProposal } from '../message-format.js';
10
+
11
+ interface Props {
12
+ operation: ProposedOperation;
13
+ class?: string;
14
+ }
15
+ let { operation, class: className }: Props = $props();
16
+ const info = $derived(describeProposal(operation));
17
+ </script>
18
+
19
+ <div
20
+ class={cn(
21
+ 'flex max-w-md gap-3 rounded-lg border border-dashed border-border px-3 py-2',
22
+ className,
23
+ )}
24
+ data-operation-id={operation.operationId}
25
+ >
26
+ <span
27
+ class="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground"
28
+ >
29
+ <Icon name={info.host === 'Excel' ? 'table' : 'file-text'} size={16} />
30
+ </span>
31
+ <div class="min-w-0 flex-1 text-sm">
32
+ <p class="font-medium text-foreground">
33
+ {info.action}{#if info.target}
34
+ <span class="font-mono text-xs text-muted-foreground"> · {info.target}</span>{/if}
35
+ </p>
36
+ <p class="text-muted-foreground [overflow-wrap:anywhere]">{operation.summary}</p>
37
+ <p class="mt-1 text-xs text-muted-foreground">Apply from the {info.host} add-in.</p>
38
+ </div>
39
+ </div>
@@ -0,0 +1,13 @@
1
+ import type { ProposedOperation } from '@classic-homes/agent-contracts';
2
+ interface Props {
3
+ operation: ProposedOperation;
4
+ class?: string;
5
+ }
6
+ /**
7
+ * Read-only view of a document edit the agent proposed. The chat never applies edits: they are
8
+ * reviewed and applied from the Excel or Word add-in, which re-reads the target first.
9
+ */
10
+ declare const ProposalCard: import("svelte").Component<Props, {}, "">;
11
+ type ProposalCard = ReturnType<typeof ProposalCard>;
12
+ export default ProposalCard;
13
+ //# sourceMappingURL=ProposalCard.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProposalCard.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ProposalCard.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAKtE,UAAU,KAAK;IACb,SAAS,EAAE,iBAAiB,CAAC;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA8BH;;;GAGG;AACH,QAAA,MAAM,YAAY,2CAAwC,CAAC;AAC3D,KAAK,YAAY,GAAG,UAAU,CAAC,OAAO,YAAY,CAAC,CAAC;AACpD,eAAe,YAAY,CAAC"}
@@ -0,0 +1,46 @@
1
+ <!--
2
+ @component
3
+ The user's turn: attachments and context above a right-aligned muted bubble, then the action bar.
4
+ -->
5
+ <script lang="ts">
6
+ import { Icon } from '@classic-homes/theme-svelte';
7
+ import type { UserMessage } from '../reducer.js';
8
+ import { fileIcon, formatBytes } from '../message-format.js';
9
+ import MessageActions from './MessageActions.svelte';
10
+
11
+ interface Props {
12
+ message: UserMessage;
13
+ last?: boolean;
14
+ }
15
+ let { message, last = false }: Props = $props();
16
+ </script>
17
+
18
+ <div class="group/message flex flex-col items-end gap-1 px-4 py-2" data-role="user">
19
+ {#if message.attachments.length || message.contextLabel}
20
+ <ul class="flex max-w-[85%] flex-wrap justify-end gap-1.5" aria-label="Attached">
21
+ {#if message.contextLabel}
22
+ <li
23
+ class="inline-flex items-center gap-1 rounded-md border border-border px-2 py-1 text-xs text-muted-foreground"
24
+ >
25
+ <Icon name="link" size={12} />
26
+ {message.contextLabel}
27
+ </li>
28
+ {/if}
29
+ {#each message.attachments as file (file.fileId)}
30
+ <li
31
+ class="inline-flex max-w-60 items-center gap-1.5 rounded-md border border-border bg-background px-2 py-1 text-xs"
32
+ >
33
+ <Icon name={fileIcon(file.mimeType)} size={14} class="shrink-0 text-muted-foreground" />
34
+ <span class="truncate text-foreground">{file.name}</span>
35
+ <span class="shrink-0 text-muted-foreground">{formatBytes(file.bytes)}</span>
36
+ </li>
37
+ {/each}
38
+ </ul>
39
+ {/if}
40
+ <div
41
+ class="max-w-[85%] whitespace-pre-wrap rounded-2xl rounded-br-md bg-muted px-4 py-2.5 text-foreground [overflow-wrap:anywhere]"
42
+ >
43
+ {message.text}
44
+ </div>
45
+ <MessageActions text={message.text} createdAt={message.createdAt} visible={last} align="end" />
46
+ </div>
@@ -0,0 +1,10 @@
1
+ import type { UserMessage } from '../reducer.js';
2
+ interface Props {
3
+ message: UserMessage;
4
+ last?: boolean;
5
+ }
6
+ /** The user's turn: attachments and context above a right-aligned muted bubble, then the action bar. */
7
+ declare const UserMessage: import("svelte").Component<Props, {}, "">;
8
+ type UserMessage = ReturnType<typeof UserMessage>;
9
+ export default UserMessage;
10
+ //# sourceMappingURL=UserMessage.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UserMessage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/UserMessage.svelte.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAK/C,UAAU,KAAK;IACb,OAAO,EAAE,WAAW,CAAC;IACrB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAqCH,wGAAwG;AACxG,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=messages.svelte.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"messages.svelte.test.d.ts","sourceRoot":"","sources":["../../src/lib/components/messages.svelte.test.ts"],"names":[],"mappings":""}