@guuey/chat 0.16.7 → 0.16.9
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 +23 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/native/markdown.d.ts +12 -0
- package/dist/native/markdown.d.ts.map +1 -1
- package/dist/native/markdown.js +21 -0
- package/dist/react/components.d.ts +1 -1
- package/dist/react/components.d.ts.map +1 -1
- package/dist/react/guuey-chat.d.ts +14 -1
- package/dist/react/guuey-chat.d.ts.map +1 -1
- package/dist/react/guuey-chat.js +27 -7
- package/dist/react/markdown.d.ts +9 -0
- package/dist/react/markdown.d.ts.map +1 -1
- package/dist/react/markdown.js +78 -2
- package/dist/react/theme-css.d.ts +8 -0
- package/dist/react/theme-css.d.ts.map +1 -1
- package/dist/react/theme-css.js +21 -13
- package/dist/react/transcript.d.ts +8 -0
- package/dist/react/transcript.d.ts.map +1 -1
- package/dist/react/transcript.js +2 -2
- package/dist/richtext-table.d.ts +26 -0
- package/dist/richtext-table.d.ts.map +1 -0
- package/dist/richtext-table.js +11 -0
- package/dist/strings.d.ts +8 -0
- package/dist/strings.d.ts.map +1 -1
- package/dist/strings.js +3 -0
- package/dist/theme.d.ts +24 -0
- package/dist/theme.d.ts.map +1 -1
- package/dist/theme.js +44 -2
- package/package.json +5 -5
- package/src/index.ts +1 -0
- package/src/native/markdown.tsx +57 -0
- package/src/react/components.tsx +1 -0
- package/src/react/guuey-chat.tsx +70 -4
- package/src/react/markdown.tsx +103 -1
- package/src/react/theme-css.ts +21 -13
- package/src/react/transcript.tsx +10 -1
- package/src/richtext-table.ts +30 -0
- package/src/strings.ts +13 -0
- package/src/theme.ts +48 -2
- package/styles.css +171 -98
package/dist/strings.js
CHANGED
|
@@ -77,6 +77,9 @@ export const defaultChatStrings = {
|
|
|
77
77
|
oauthConnected: (serverName) => `Connected ${serverName}. The agent can use it from your next message.`,
|
|
78
78
|
oauthFailed: (reason) => `Couldn't connect: ${reason}`,
|
|
79
79
|
noticeLabel: "Note",
|
|
80
|
+
linkAskLabel: (host) => `This card wants to open ${host}`,
|
|
81
|
+
linkOpen: "Open",
|
|
82
|
+
linkDismiss: "Dismiss",
|
|
80
83
|
composerPlaceholder: "Message the agent…",
|
|
81
84
|
composerUnavailable: "Chat is unavailable.",
|
|
82
85
|
composerLabel: "Message",
|
package/dist/theme.d.ts
CHANGED
|
@@ -246,6 +246,14 @@ export type GuueyChatTheme = z.infer<typeof GuueyChatTheme>;
|
|
|
246
246
|
* The brand-neutral-but-polished package default — the theme a builder gets
|
|
247
247
|
* before configuring anything, and the per-token fallback floor every other
|
|
248
248
|
* theme resolves against.
|
|
249
|
+
*
|
|
250
|
+
* The accent is MONOCHROME (= ink) by founder ruling (guuey#521): an
|
|
251
|
+
* unthemed embed must never carry a foreign accent into a host's product —
|
|
252
|
+
* the old `#2f6bff` blue made every zero-config embed read "off-the-shelf
|
|
253
|
+
* chat vendor" inside someone else's brand (#414's lesson, mirrored). Ink
|
|
254
|
+
* as accent means the send button and user pill render as neutral
|
|
255
|
+
* ink-on-canvas and disappear into any host; a brand accent is a CHOICE
|
|
256
|
+
* (theme prop or one `--guuey-chat-accent` CSS variable), never a default.
|
|
249
257
|
*/
|
|
250
258
|
export declare const DEFAULT_CHAT_THEME: GuueyChatTheme;
|
|
251
259
|
/**
|
|
@@ -262,4 +270,20 @@ export declare const GUUEY_CHAT_THEME: GuueyChatTheme;
|
|
|
262
270
|
* NEVER throws: unparseable input resolves to the base theme untouched.
|
|
263
271
|
*/
|
|
264
272
|
export declare function resolveTheme(candidate: unknown, base?: GuueyChatTheme): GuueyChatTheme;
|
|
273
|
+
/**
|
|
274
|
+
* Per-court theme resolution (guuey#519 — the #414 rule generalized).
|
|
275
|
+
*
|
|
276
|
+
* A theme document may carry `courts`: explicit per-court override
|
|
277
|
+
* documents keyed by serving court (`"guuey"`, `"ggui"`, …). A surface
|
|
278
|
+
* resolves ITS court: the court's override resolved per-token OVER the
|
|
279
|
+
* resolved base document when declared, else the base alone. **Brand is
|
|
280
|
+
* never a default** — an undeclared or unknown court gets the neutral
|
|
281
|
+
* base by construction, and readers that never heard of `courts`
|
|
282
|
+
* (lenient parsers projecting only known tokens) keep reading the base
|
|
283
|
+
* untouched, so no court can inherit another court's brand.
|
|
284
|
+
*
|
|
285
|
+
* Resolved themes are court-free: `courts` never survives resolution.
|
|
286
|
+
* NEVER throws; unparseable input degrades exactly as `resolveTheme`.
|
|
287
|
+
*/
|
|
288
|
+
export declare function resolveCourtTheme(candidate: unknown, court: string, base?: GuueyChatTheme): GuueyChatTheme;
|
|
265
289
|
//# sourceMappingURL=theme.d.ts.map
|
package/dist/theme.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;iBAWnB,CAAC;AACX,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;iBAUtB,CAAC;AACX,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,eAAO,MAAM,kBAAkB;;;;iBAMrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,eAAO,MAAM,iBAAiB;;iBAAqD,CAAC;AACpF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;iBAQnB,CAAC;AACX,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAKjB,CAAC;AACX,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BjB,CAAC;AACX,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../src/theme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;GAGG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;iBAWnB,CAAC;AACX,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB;;;;;;;;iBAUtB,CAAC;AACX,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,eAAO,MAAM,kBAAkB;;;;iBAMrB,CAAC;AACX,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAEpE,eAAO,MAAM,iBAAiB;;iBAAqD,CAAC;AACpF,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE,+DAA+D;AAC/D,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;iBAQnB,CAAC;AACX,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAKjB,CAAC;AACX,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BjB,CAAC;AACX,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,kBAAkB,EAAE,cA0BhC,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,EAAE,cA0B9B,CAAC;AA4CF;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,SAAS,EAAE,OAAO,EAClB,IAAI,GAAE,cAAmC,GACxC,cAAc,CAuBhB;AAYD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,OAAO,EAClB,KAAK,EAAE,MAAM,EACb,IAAI,GAAE,cAAmC,GACxC,cAAc,CAOhB"}
|
package/dist/theme.js
CHANGED
|
@@ -115,12 +115,20 @@ export const GuueyChatTheme = z
|
|
|
115
115
|
* The brand-neutral-but-polished package default — the theme a builder gets
|
|
116
116
|
* before configuring anything, and the per-token fallback floor every other
|
|
117
117
|
* theme resolves against.
|
|
118
|
+
*
|
|
119
|
+
* The accent is MONOCHROME (= ink) by founder ruling (guuey#521): an
|
|
120
|
+
* unthemed embed must never carry a foreign accent into a host's product —
|
|
121
|
+
* the old `#2f6bff` blue made every zero-config embed read "off-the-shelf
|
|
122
|
+
* chat vendor" inside someone else's brand (#414's lesson, mirrored). Ink
|
|
123
|
+
* as accent means the send button and user pill render as neutral
|
|
124
|
+
* ink-on-canvas and disappear into any host; a brand accent is a CHOICE
|
|
125
|
+
* (theme prop or one `--guuey-chat-accent` CSS variable), never a default.
|
|
118
126
|
*/
|
|
119
127
|
export const DEFAULT_CHAT_THEME = {
|
|
120
128
|
name: "default",
|
|
121
129
|
colors: {
|
|
122
130
|
light: {
|
|
123
|
-
accent: "#
|
|
131
|
+
accent: "#111318",
|
|
124
132
|
onAccent: "#ffffff",
|
|
125
133
|
ink: "#111318",
|
|
126
134
|
inkMuted: "#5b6270",
|
|
@@ -130,7 +138,7 @@ export const DEFAULT_CHAT_THEME = {
|
|
|
130
138
|
error: "#d64545",
|
|
131
139
|
},
|
|
132
140
|
dark: {
|
|
133
|
-
accent: "#
|
|
141
|
+
accent: "#e8e9ee",
|
|
134
142
|
onAccent: "#0b0d12",
|
|
135
143
|
ink: "#e8e9ee",
|
|
136
144
|
inkMuted: "#9aa0ac",
|
|
@@ -245,6 +253,40 @@ export function resolveTheme(candidate, base = DEFAULT_CHAT_THEME) {
|
|
|
245
253
|
},
|
|
246
254
|
};
|
|
247
255
|
}
|
|
256
|
+
/**
|
|
257
|
+
* The court-override member a theme DOCUMENT may carry (guuey#519).
|
|
258
|
+
* Values are theme documents themselves (validated by `resolveTheme`'s own
|
|
259
|
+
* lenient parse at resolution time, so a partial or future-schema court
|
|
260
|
+
* entry degrades per-token like any stored theme).
|
|
261
|
+
*/
|
|
262
|
+
const CourtOverrides = z
|
|
263
|
+
.object({ courts: z.record(z.string(), z.unknown()).optional() })
|
|
264
|
+
.loose();
|
|
265
|
+
/**
|
|
266
|
+
* Per-court theme resolution (guuey#519 — the #414 rule generalized).
|
|
267
|
+
*
|
|
268
|
+
* A theme document may carry `courts`: explicit per-court override
|
|
269
|
+
* documents keyed by serving court (`"guuey"`, `"ggui"`, …). A surface
|
|
270
|
+
* resolves ITS court: the court's override resolved per-token OVER the
|
|
271
|
+
* resolved base document when declared, else the base alone. **Brand is
|
|
272
|
+
* never a default** — an undeclared or unknown court gets the neutral
|
|
273
|
+
* base by construction, and readers that never heard of `courts`
|
|
274
|
+
* (lenient parsers projecting only known tokens) keep reading the base
|
|
275
|
+
* untouched, so no court can inherit another court's brand.
|
|
276
|
+
*
|
|
277
|
+
* Resolved themes are court-free: `courts` never survives resolution.
|
|
278
|
+
* NEVER throws; unparseable input degrades exactly as `resolveTheme`.
|
|
279
|
+
*/
|
|
280
|
+
export function resolveCourtTheme(candidate, court, base = DEFAULT_CHAT_THEME) {
|
|
281
|
+
const resolvedBase = resolveTheme(candidate, base);
|
|
282
|
+
const parsed = CourtOverrides.safeParse(candidate);
|
|
283
|
+
if (!parsed.success)
|
|
284
|
+
return resolvedBase;
|
|
285
|
+
const override = parsed.data.courts?.[court];
|
|
286
|
+
if (override === undefined)
|
|
287
|
+
return resolvedBase;
|
|
288
|
+
return resolveTheme(override, resolvedBase);
|
|
289
|
+
}
|
|
248
290
|
/** Slot-level merge of two ramp statements (candidate slots win per slot). */
|
|
249
291
|
function mergeRampSet(base, over) {
|
|
250
292
|
if (base === undefined)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@guuey/chat",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.9",
|
|
4
4
|
"description": "The default end-user transcript UI for guuey agents — the headless view-model (planTranscript), calm/debug presets, the GuueyChatTheme token schema, the ChatStrings i18n seam, and (at ./react) the component kit that renders it: per-category components, <Transcript>, the live + history input assemblers, and stick-to-bottom/windowed/accessible transcript behavior; at ./native, the React Native renderer over the same view-model.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -36,11 +36,11 @@
|
|
|
36
36
|
"./styles.css": "./styles.css"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@silverprotocol/core": "0.5.
|
|
40
|
-
"@silverprotocol/richtext": "0.5.
|
|
39
|
+
"@silverprotocol/core": "0.5.3",
|
|
40
|
+
"@silverprotocol/richtext": "0.5.3",
|
|
41
41
|
"zod": "^4.3.6",
|
|
42
|
-
"@guuey/agent-client": "0.16.
|
|
43
|
-
"@guuey/mcp-apps-host": "0.16.
|
|
42
|
+
"@guuey/agent-client": "0.16.9",
|
|
43
|
+
"@guuey/mcp-apps-host": "0.16.9"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
46
|
"react": ">=18",
|
package/src/index.ts
CHANGED
package/src/native/markdown.tsx
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type RichTextInline,
|
|
27
27
|
} from "@silverprotocol/richtext";
|
|
28
28
|
import type { NativeChatTokens } from "./theme-native.js";
|
|
29
|
+
import { normalizeTableRow, type RichTextTableBlock } from "../richtext-table.js";
|
|
29
30
|
|
|
30
31
|
function InlineRuns({
|
|
31
32
|
nodes,
|
|
@@ -120,6 +121,8 @@ function BlockView({
|
|
|
120
121
|
trailing?: ReactNode;
|
|
121
122
|
}): ReactNode {
|
|
122
123
|
switch (block.type) {
|
|
124
|
+
case "table":
|
|
125
|
+
return <NativeTableBlock block={block} color={color} tokens={tokens} trailing={trailing} />;
|
|
123
126
|
case "paragraph":
|
|
124
127
|
return (
|
|
125
128
|
<Text style={{ fontSize: tokens.fontSize, lineHeight: Math.round(tokens.fontSize * 1.45) }}>
|
|
@@ -175,6 +178,60 @@ function BlockView({
|
|
|
175
178
|
}
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
/**
|
|
182
|
+
* The native table projection (guuey#370): header row bold with a
|
|
183
|
+
* separator, body rows as flex rows, per-column alignment mapped to RN
|
|
184
|
+
* textAlign. Exported for tests; not part of the package surface.
|
|
185
|
+
*/
|
|
186
|
+
export function NativeTableBlock({
|
|
187
|
+
block,
|
|
188
|
+
color,
|
|
189
|
+
tokens,
|
|
190
|
+
trailing,
|
|
191
|
+
}: {
|
|
192
|
+
block: RichTextTableBlock;
|
|
193
|
+
color: string;
|
|
194
|
+
tokens: NativeChatTokens;
|
|
195
|
+
trailing?: ReactNode;
|
|
196
|
+
}): ReactNode {
|
|
197
|
+
const columnCount = block.header.cells.length;
|
|
198
|
+
const cellStyle = (i: number) => ({
|
|
199
|
+
flex: 1,
|
|
200
|
+
...(block.align[i] !== undefined ? { textAlign: block.align[i] } : {}),
|
|
201
|
+
});
|
|
202
|
+
return (
|
|
203
|
+
<View style={{ gap: 2 }}>
|
|
204
|
+
<View
|
|
205
|
+
style={{
|
|
206
|
+
flexDirection: "row",
|
|
207
|
+
borderBottomWidth: 1,
|
|
208
|
+
borderBottomColor: tokens.palette.canvasMuted,
|
|
209
|
+
paddingBottom: 2,
|
|
210
|
+
}}
|
|
211
|
+
>
|
|
212
|
+
{normalizeTableRow(block.header, columnCount).map((cell, i) => (
|
|
213
|
+
<Text
|
|
214
|
+
key={i}
|
|
215
|
+
style={{ ...cellStyle(i), color, fontSize: tokens.fontSize, fontWeight: "700" }}
|
|
216
|
+
>
|
|
217
|
+
<InlineRuns nodes={cell.children} color={color} tokens={tokens} bold />
|
|
218
|
+
</Text>
|
|
219
|
+
))}
|
|
220
|
+
</View>
|
|
221
|
+
{block.rows.map((row, r) => (
|
|
222
|
+
<View key={r} style={{ flexDirection: "row" }}>
|
|
223
|
+
{normalizeTableRow(row, columnCount).map((cell, i) => (
|
|
224
|
+
<Text key={i} style={{ ...cellStyle(i), fontSize: tokens.fontSize }}>
|
|
225
|
+
<InlineRuns nodes={cell.children} color={color} tokens={tokens} />
|
|
226
|
+
</Text>
|
|
227
|
+
))}
|
|
228
|
+
</View>
|
|
229
|
+
))}
|
|
230
|
+
{trailing}
|
|
231
|
+
</View>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
178
235
|
/** Sanitized markdown → themed RN elements (see the module docblock). */
|
|
179
236
|
export function NativeMarkdown({
|
|
180
237
|
text,
|
package/src/react/components.tsx
CHANGED
package/src/react/guuey-chat.tsx
CHANGED
|
@@ -93,7 +93,10 @@ import { oauthPromptAction, useOAuthReturn } from "./oauth-return.js";
|
|
|
93
93
|
export function viewPropsWithThemeAnnounce(
|
|
94
94
|
viewProps: TranscriptItemContext["viewProps"],
|
|
95
95
|
mode: ThemeMode,
|
|
96
|
-
defaults: Pick<
|
|
96
|
+
defaults: Pick<
|
|
97
|
+
ViewSlotProps,
|
|
98
|
+
"onCallTool" | "onUpdateModelContext" | "onUserMessage" | "onOpenLink"
|
|
99
|
+
> = {},
|
|
97
100
|
): TranscriptItemContext["viewProps"] {
|
|
98
101
|
const themed = (base: ViewSlotProps | undefined): ViewSlotProps => ({
|
|
99
102
|
// Kit-default host wires (guuey#335): the ACTION RELAY (Confirm inside
|
|
@@ -214,6 +217,19 @@ export interface GuueyChatProps {
|
|
|
214
217
|
strings?: Partial<ChatStrings>;
|
|
215
218
|
theme?: GuueyChatTheme;
|
|
216
219
|
mode?: ThemeMode;
|
|
220
|
+
/**
|
|
221
|
+
* Message-surface presentation (guuey#521). `"card"` (default) keeps
|
|
222
|
+
* assistant text on its own card; `"bare"` sits the transcript directly
|
|
223
|
+
* on the host's background so the embed reads native to the page.
|
|
224
|
+
*/
|
|
225
|
+
surface?: "card" | "bare";
|
|
226
|
+
/**
|
|
227
|
+
* Render the built-in composer (default `true`). `false` hides it for
|
|
228
|
+
* hosts that drive the conversation through the imperative handle
|
|
229
|
+
* (guuey#210 `ask`/`compose`) or their own input — pairs with the
|
|
230
|
+
* handle instead of forcing a CSS hide (guuey#521).
|
|
231
|
+
*/
|
|
232
|
+
composer?: boolean;
|
|
217
233
|
/** DOM windowing (§3.2). `false` renders everything. */
|
|
218
234
|
window?: TranscriptWindowing | false;
|
|
219
235
|
/**
|
|
@@ -323,6 +339,8 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
323
339
|
strings: stringOverrides,
|
|
324
340
|
theme = DEFAULT_CHAT_THEME,
|
|
325
341
|
mode = "light",
|
|
342
|
+
surface = "card",
|
|
343
|
+
composer = true,
|
|
326
344
|
window: windowing,
|
|
327
345
|
reader,
|
|
328
346
|
onDebugEvent,
|
|
@@ -546,8 +564,9 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
546
564
|
// REAL result envelope and its #440 classifier grades honestly; the
|
|
547
565
|
// post-turn agent turn then starts through the ui/message doorbell
|
|
548
566
|
// (the sink below). The #356 staging seam remains a PUBLIC export
|
|
549
|
-
// (withActionStaging) for hosts that choose the composer-staging UX
|
|
550
|
-
//
|
|
567
|
+
// (withActionStaging) for hosts that choose the composer-staging UX;
|
|
568
|
+
// no first-party surface applies it any more (the widget converged on
|
|
569
|
+
// this relay+doorbell path in guuey#404).
|
|
551
570
|
const stagedDefaultOnCallTool = defaultOnCallTool;
|
|
552
571
|
|
|
553
572
|
// The ui/message sink (guuey#422): the view hands the host role-user
|
|
@@ -620,14 +639,33 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
620
639
|
void live.invoke.send(next).catch(() => {});
|
|
621
640
|
}, [busy]);
|
|
622
641
|
|
|
642
|
+
// guuey#522: the kit default for `ui/open-link` NEVER navigates
|
|
643
|
+
// sight-unseen — the ask surfaces as a disclosure affordance (host +
|
|
644
|
+
// full URL + Open/Dismiss) and the OPEN is the human's own anchor
|
|
645
|
+
// click, so the browser's activation gate and the review posture agree
|
|
646
|
+
// by construction. ONE pending ask at a time, newest replaces (a
|
|
647
|
+
// card spamming asks can never grow an unbounded stack).
|
|
648
|
+
const [pendingLink, setPendingLink] = useState<string | null>(null);
|
|
649
|
+
const defaultOnOpenLink = useCallback((url: string) => {
|
|
650
|
+
setPendingLink(url);
|
|
651
|
+
}, []);
|
|
652
|
+
|
|
623
653
|
const effectiveViewProps = useMemo<TranscriptItemContext["viewProps"]>(
|
|
624
654
|
() =>
|
|
625
655
|
viewPropsWithThemeAnnounce(viewProps, mode, {
|
|
626
656
|
...(stagedDefaultOnCallTool !== undefined ? { onCallTool: stagedDefaultOnCallTool } : {}),
|
|
627
657
|
onUpdateModelContext: defaultOnUpdateModelContext,
|
|
628
658
|
onUserMessage: defaultOnUserMessage,
|
|
659
|
+
onOpenLink: defaultOnOpenLink,
|
|
629
660
|
}),
|
|
630
|
-
[
|
|
661
|
+
[
|
|
662
|
+
viewProps,
|
|
663
|
+
mode,
|
|
664
|
+
stagedDefaultOnCallTool,
|
|
665
|
+
defaultOnUpdateModelContext,
|
|
666
|
+
defaultOnUserMessage,
|
|
667
|
+
defaultOnOpenLink,
|
|
668
|
+
],
|
|
631
669
|
);
|
|
632
670
|
|
|
633
671
|
// The canvas-host door (guuey#335): a host mounting views DIRECTLY from
|
|
@@ -643,6 +681,7 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
643
681
|
...(stagedDefaultOnCallTool !== undefined ? { onCallTool: stagedDefaultOnCallTool } : {}),
|
|
644
682
|
onUpdateModelContext: defaultOnUpdateModelContext,
|
|
645
683
|
onUserMessage: defaultOnUserMessage,
|
|
684
|
+
onOpenLink: defaultOnOpenLink,
|
|
646
685
|
hostContext: { theme: mode },
|
|
647
686
|
}
|
|
648
687
|
: (effectiveViewProps ?? {});
|
|
@@ -776,6 +815,7 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
776
815
|
strings={strings}
|
|
777
816
|
theme={theme}
|
|
778
817
|
mode={mode}
|
|
818
|
+
surface={surface}
|
|
779
819
|
{...(windowing !== undefined ? { window: windowing } : {})}
|
|
780
820
|
{...(components !== undefined ? { components } : {})}
|
|
781
821
|
onToggle={toggle}
|
|
@@ -788,6 +828,30 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
788
828
|
{...(onViewRef !== undefined ? { onViewRef } : {})}
|
|
789
829
|
viewProps={effectiveViewProps}
|
|
790
830
|
/>
|
|
831
|
+
{pendingLink !== null && (
|
|
832
|
+
<div role="status" className="guuey-chat-link-ask">
|
|
833
|
+
<span className="guuey-chat-link-ask-label">
|
|
834
|
+
{strings.linkAskLabel(new URL(pendingLink).host)}
|
|
835
|
+
</span>
|
|
836
|
+
<span className="guuey-chat-link-ask-url">{pendingLink}</span>
|
|
837
|
+
<a
|
|
838
|
+
className="guuey-chat-link-ask-open"
|
|
839
|
+
href={pendingLink}
|
|
840
|
+
target="_blank"
|
|
841
|
+
rel="noopener noreferrer"
|
|
842
|
+
onClick={() => setPendingLink(null)}
|
|
843
|
+
>
|
|
844
|
+
{strings.linkOpen}
|
|
845
|
+
</a>
|
|
846
|
+
<button
|
|
847
|
+
type="button"
|
|
848
|
+
className="guuey-chat-link-ask-dismiss"
|
|
849
|
+
onClick={() => setPendingLink(null)}
|
|
850
|
+
>
|
|
851
|
+
{strings.linkDismiss}
|
|
852
|
+
</button>
|
|
853
|
+
</div>
|
|
854
|
+
)}
|
|
791
855
|
{oauthReturn.notice !== null && (
|
|
792
856
|
<p
|
|
793
857
|
role="status"
|
|
@@ -801,6 +865,7 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
801
865
|
</button>
|
|
802
866
|
</p>
|
|
803
867
|
)}
|
|
868
|
+
{composer && (
|
|
804
869
|
<form
|
|
805
870
|
className="guuey-chat-composer"
|
|
806
871
|
onSubmit={(e) => {
|
|
@@ -842,6 +907,7 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
842
907
|
</button>
|
|
843
908
|
)}
|
|
844
909
|
</form>
|
|
910
|
+
)}
|
|
845
911
|
</div>
|
|
846
912
|
);
|
|
847
913
|
});
|
package/src/react/markdown.tsx
CHANGED
|
@@ -30,12 +30,77 @@ import {
|
|
|
30
30
|
type RichTextBlock,
|
|
31
31
|
type RichTextInline,
|
|
32
32
|
} from "@silverprotocol/richtext";
|
|
33
|
+
import { normalizeTableRow, type RichTextTableBlock } from "../richtext-table.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Bare-URL autolink (guuey#515): richtext mints `link` nodes only from
|
|
37
|
+
* explicit `[text](url)` markdown, so a plain "https://…" in agent prose
|
|
38
|
+
* parses as a text node and rendered as untappable static text — on a
|
|
39
|
+
* phone that means retyping a URL out of a chat bubble. Linkify at the
|
|
40
|
+
* render boundary, conservatively:
|
|
41
|
+
* - the scheme is part of the match (http/https only), so no
|
|
42
|
+
* `javascript:`/`data:` target can ever qualify — the SAFE_HREF
|
|
43
|
+
* property holds by construction;
|
|
44
|
+
* - built as React elements (no HTML string exists to inject into),
|
|
45
|
+
* same `rel`/`target` contract as explicit links;
|
|
46
|
+
* - trailing punctuation stays prose ("see https://guuey.com." must not
|
|
47
|
+
* link the dot), while a balanced closing paren/bracket stays in the
|
|
48
|
+
* URL (wiki-style paths);
|
|
49
|
+
* - schemeless "www.foo.com" is deliberately NOT linkified — guessing
|
|
50
|
+
* schemes is how autolinkers start lying;
|
|
51
|
+
* - code spans are untouched (they render `node.code`, not this path).
|
|
52
|
+
*/
|
|
53
|
+
const BARE_URL = /https?:\/\/[^\s<>]+/g;
|
|
54
|
+
|
|
55
|
+
function trimTrailingPunctuation(url: string): string {
|
|
56
|
+
for (;;) {
|
|
57
|
+
const last = url[url.length - 1];
|
|
58
|
+
if (last === undefined) return url;
|
|
59
|
+
if (last === ")" || last === "]") {
|
|
60
|
+
const open = last === ")" ? "(" : "[";
|
|
61
|
+
const opens = url.split(open).length - 1;
|
|
62
|
+
const closes = url.split(last).length - 1;
|
|
63
|
+
if (closes > opens) {
|
|
64
|
+
url = url.slice(0, -1);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
return url;
|
|
68
|
+
}
|
|
69
|
+
if (".,;:!?'\"»›".includes(last)) {
|
|
70
|
+
url = url.slice(0, -1);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
return url;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function linkify(text: string): ReactNode {
|
|
78
|
+
if (!text.includes("http")) return text;
|
|
79
|
+
const out: ReactNode[] = [];
|
|
80
|
+
let consumed = 0;
|
|
81
|
+
for (const match of text.matchAll(BARE_URL)) {
|
|
82
|
+
const start = match.index;
|
|
83
|
+
if (start === undefined) continue;
|
|
84
|
+
const url = trimTrailingPunctuation(match[0]);
|
|
85
|
+
if (url.length === 0) continue;
|
|
86
|
+
if (start > consumed) out.push(text.slice(consumed, start));
|
|
87
|
+
out.push(
|
|
88
|
+
<a key={`url-${start}`} href={url} target="_blank" rel="noopener noreferrer">
|
|
89
|
+
{url}
|
|
90
|
+
</a>,
|
|
91
|
+
);
|
|
92
|
+
consumed = start + url.length;
|
|
93
|
+
}
|
|
94
|
+
if (out.length === 0) return text;
|
|
95
|
+
if (consumed < text.length) out.push(text.slice(consumed));
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
33
98
|
|
|
34
99
|
function Inline({ nodes }: { nodes: RichTextInline[] }): ReactNode {
|
|
35
100
|
return nodes.map((node, i) => {
|
|
36
101
|
switch (node.type) {
|
|
37
102
|
case "text":
|
|
38
|
-
return <span key={i}>{node.text}</span>;
|
|
103
|
+
return <span key={i}>{linkify(node.text)}</span>;
|
|
39
104
|
case "break":
|
|
40
105
|
return <br key={i} />;
|
|
41
106
|
case "strong":
|
|
@@ -71,8 +136,45 @@ function Inline({ nodes }: { nodes: RichTextInline[] }): ReactNode {
|
|
|
71
136
|
});
|
|
72
137
|
}
|
|
73
138
|
|
|
139
|
+
/**
|
|
140
|
+
* The table arm (guuey#370 — sdk#23's node). Cells render through the
|
|
141
|
+
* SAME `<Inline>` sanitizer path as every other run. Exported for tests;
|
|
142
|
+
* not part of the package surface.
|
|
143
|
+
*/
|
|
144
|
+
export function TableBlock({ block }: { block: RichTextTableBlock }): ReactNode {
|
|
145
|
+
const columnCount = block.header.cells.length;
|
|
146
|
+
return (
|
|
147
|
+
<div className="guuey-chat-table-wrap">
|
|
148
|
+
<table className="guuey-chat-table">
|
|
149
|
+
<thead>
|
|
150
|
+
<tr>
|
|
151
|
+
{normalizeTableRow(block.header, columnCount).map((cell, i) => (
|
|
152
|
+
<th key={i} {...(block.align[i] !== undefined ? { style: { textAlign: block.align[i] } } : {})}>
|
|
153
|
+
<Inline nodes={cell.children} />
|
|
154
|
+
</th>
|
|
155
|
+
))}
|
|
156
|
+
</tr>
|
|
157
|
+
</thead>
|
|
158
|
+
<tbody>
|
|
159
|
+
{block.rows.map((row, r) => (
|
|
160
|
+
<tr key={r}>
|
|
161
|
+
{normalizeTableRow(row, columnCount).map((cell, i) => (
|
|
162
|
+
<td key={i} {...(block.align[i] !== undefined ? { style: { textAlign: block.align[i] } } : {})}>
|
|
163
|
+
<Inline nodes={cell.children} />
|
|
164
|
+
</td>
|
|
165
|
+
))}
|
|
166
|
+
</tr>
|
|
167
|
+
))}
|
|
168
|
+
</tbody>
|
|
169
|
+
</table>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
74
174
|
function Block({ block }: { block: RichTextBlock }): ReactNode {
|
|
75
175
|
switch (block.type) {
|
|
176
|
+
case "table":
|
|
177
|
+
return <TableBlock block={block} />;
|
|
76
178
|
case "paragraph":
|
|
77
179
|
return (
|
|
78
180
|
<p>
|
package/src/react/theme-css.ts
CHANGED
|
@@ -24,27 +24,35 @@ const DENSITY_GAP: Record<GuueyChatTheme["shape"]["density"], string> = {
|
|
|
24
24
|
* One mode's tokens as inline custom properties for the transcript root.
|
|
25
25
|
* Returned as a plain record so callers can spread it into `style` or emit
|
|
26
26
|
* a stylesheet from it.
|
|
27
|
+
*
|
|
28
|
+
* Stamped under INTERNAL names (`--_guuey-chat-*`), never the documented
|
|
29
|
+
* `--guuey-chat-*` channel (guuey#521): the stylesheet reads every token
|
|
30
|
+
* as `var(--guuey-chat-<t>, var(--_guuey-chat-<t>))`, so a host CSS
|
|
31
|
+
* variable — set on any ancestor — wins per-token over the resolved
|
|
32
|
+
* theme, and the two theming channels COMPOSE instead of the inline
|
|
33
|
+
* stamp silently shadowing the documented one (the trap the landing
|
|
34
|
+
* home-hero embed hit four rounds of).
|
|
27
35
|
*/
|
|
28
36
|
export function themeCssVars(theme: GuueyChatTheme, mode: ThemeMode): Record<string, string> {
|
|
29
37
|
const palette = theme.colors[mode];
|
|
30
38
|
const vars: Record<string, string> = {
|
|
31
|
-
"--
|
|
32
|
-
"--
|
|
33
|
-
"--
|
|
34
|
-
"--
|
|
35
|
-
"--
|
|
36
|
-
"--
|
|
37
|
-
"--
|
|
38
|
-
"--
|
|
39
|
-
"--
|
|
40
|
-
"--
|
|
41
|
-
"--
|
|
39
|
+
"--_guuey-chat-accent": palette.accent,
|
|
40
|
+
"--_guuey-chat-on-accent": palette.onAccent,
|
|
41
|
+
"--_guuey-chat-ink": palette.ink,
|
|
42
|
+
"--_guuey-chat-ink-muted": palette.inkMuted,
|
|
43
|
+
"--_guuey-chat-surface": palette.surface,
|
|
44
|
+
"--_guuey-chat-canvas": palette.canvas,
|
|
45
|
+
"--_guuey-chat-canvas-muted": palette.canvasMuted,
|
|
46
|
+
"--_guuey-chat-error": palette.error,
|
|
47
|
+
"--_guuey-chat-radius": RADIUS_PX[theme.shape.radius],
|
|
48
|
+
"--_guuey-chat-gap": DENSITY_GAP[theme.shape.density],
|
|
49
|
+
"--_guuey-chat-scale": String(theme.typography.scale ?? 1),
|
|
42
50
|
};
|
|
43
51
|
if (theme.typography.fontFamily !== undefined) {
|
|
44
|
-
vars["--
|
|
52
|
+
vars["--_guuey-chat-font"] = theme.typography.fontFamily;
|
|
45
53
|
}
|
|
46
54
|
if (theme.typography.monoFontFamily !== undefined) {
|
|
47
|
-
vars["--
|
|
55
|
+
vars["--_guuey-chat-mono-font"] = theme.typography.monoFontFamily;
|
|
48
56
|
}
|
|
49
57
|
return vars;
|
|
50
58
|
}
|
package/src/react/transcript.tsx
CHANGED
|
@@ -63,6 +63,14 @@ export interface TranscriptProps
|
|
|
63
63
|
strings?: ChatStrings;
|
|
64
64
|
theme?: GuueyChatTheme;
|
|
65
65
|
mode?: ThemeMode;
|
|
66
|
+
/**
|
|
67
|
+
* Message-surface presentation (guuey#521). `"card"` (default) renders
|
|
68
|
+
* assistant text on its own surface-tinted, hairline-bordered card;
|
|
69
|
+
* `"bare"` sits the transcript directly on the host's background — the
|
|
70
|
+
* embed disappears into the page it lives on. The user pill stays a
|
|
71
|
+
* bubble in both.
|
|
72
|
+
*/
|
|
73
|
+
surface?: "card" | "bare";
|
|
66
74
|
/** DOM windowing (§3.2). `false` renders everything. Default tail 80. */
|
|
67
75
|
window?: TranscriptWindowing | false;
|
|
68
76
|
className?: string;
|
|
@@ -76,6 +84,7 @@ export function Transcript(props: TranscriptProps): ReactNode {
|
|
|
76
84
|
strings = defaultChatStrings,
|
|
77
85
|
theme = DEFAULT_CHAT_THEME,
|
|
78
86
|
mode = "light",
|
|
87
|
+
surface = "card",
|
|
79
88
|
window: windowing = { tail: 80 },
|
|
80
89
|
className,
|
|
81
90
|
style,
|
|
@@ -169,7 +178,7 @@ export function Transcript(props: TranscriptProps): ReactNode {
|
|
|
169
178
|
|
|
170
179
|
return (
|
|
171
180
|
<div
|
|
172
|
-
className={`guuey-chat${className !== undefined ? ` ${className}` : ""}`}
|
|
181
|
+
className={`guuey-chat${surface === "bare" ? " guuey-chat--bare" : ""}${className !== undefined ? ` ${className}` : ""}`}
|
|
173
182
|
style={rootStyle}
|
|
174
183
|
data-guuey-chat-mode={mode}
|
|
175
184
|
>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Table rendering support (guuey#370 — sdk#23's vocabulary, live since
|
|
3
|
+
* `@silverprotocol/richtext@0.5.3`).
|
|
4
|
+
*
|
|
5
|
+
* The block type DERIVES from the upstream union (strict-typing rule:
|
|
6
|
+
* the parser's types are the source of truth). History note: this module
|
|
7
|
+
* was born one cut earlier as a structural MIRROR + narrowing guard so
|
|
8
|
+
* both renderer arms could land BEFORE the bump (the renderers switch
|
|
9
|
+
* default-less on `block.type` — a bump without an arm would have
|
|
10
|
+
* rendered tables as NOTHING); with 0.5.3 pinned, the mirror and guard
|
|
11
|
+
* are deleted per the pre-launch one-contract rule and the arms moved
|
|
12
|
+
* into the switches proper.
|
|
13
|
+
*/
|
|
14
|
+
import type { RichTextBlock, RichTextTableCell } from "@silverprotocol/richtext";
|
|
15
|
+
|
|
16
|
+
export type RichTextTableBlock = Extract<RichTextBlock, { type: "table" }>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Renderer-side GFM normalization (defense in depth — the parser already
|
|
20
|
+
* pads/drops): every row exactly `columnCount` cells, short rows padded
|
|
21
|
+
* with empty-inline cells, excess dropped.
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeTableRow(
|
|
24
|
+
row: { cells: RichTextTableCell[] },
|
|
25
|
+
columnCount: number,
|
|
26
|
+
): RichTextTableCell[] {
|
|
27
|
+
const cells = row.cells.slice(0, columnCount);
|
|
28
|
+
while (cells.length < columnCount) cells.push({ children: [] });
|
|
29
|
+
return cells;
|
|
30
|
+
}
|
package/src/strings.ts
CHANGED
|
@@ -128,6 +128,15 @@ export interface ChatStrings {
|
|
|
128
128
|
/** R16 — the notice row's label (provenance shows only under debug). */
|
|
129
129
|
noticeLabel: string;
|
|
130
130
|
|
|
131
|
+
/**
|
|
132
|
+
* The `ui/open-link` disclosure affordance (guuey#522): a card asked to
|
|
133
|
+
* open a URL — the kit shows WHERE before anything navigates, and the
|
|
134
|
+
* human's own click on `linkOpen` is the only door out.
|
|
135
|
+
*/
|
|
136
|
+
linkAskLabel: (host: string) => string;
|
|
137
|
+
linkOpen: string;
|
|
138
|
+
linkDismiss: string;
|
|
139
|
+
|
|
131
140
|
/** The 3c composer (`<GuueyChat>`). */
|
|
132
141
|
composerPlaceholder: string;
|
|
133
142
|
composerUnavailable: string;
|
|
@@ -233,6 +242,10 @@ export const defaultChatStrings: ChatStrings = {
|
|
|
233
242
|
|
|
234
243
|
noticeLabel: "Note",
|
|
235
244
|
|
|
245
|
+
linkAskLabel: (host) => `This card wants to open ${host}`,
|
|
246
|
+
linkOpen: "Open",
|
|
247
|
+
linkDismiss: "Dismiss",
|
|
248
|
+
|
|
236
249
|
composerPlaceholder: "Message the agent…",
|
|
237
250
|
composerUnavailable: "Chat is unavailable.",
|
|
238
251
|
composerLabel: "Message",
|