@guuey/chat 0.9.0 → 0.11.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/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js +62 -9
- package/dist/policy.d.ts +9 -1
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +1 -1
- package/dist/react/components.d.ts.map +1 -1
- package/dist/react/components.js +3 -2
- 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 +76 -15
- package/dist/react/structural-identity.d.ts +3 -0
- package/dist/react/structural-identity.d.ts.map +1 -0
- package/dist/react/structural-identity.js +45 -0
- package/dist/react/use-transcript.d.ts.map +1 -1
- package/dist/react/use-transcript.js +5 -3
- package/dist/strings.d.ts +4 -0
- package/dist/strings.d.ts.map +1 -1
- package/dist/strings.js +2 -0
- package/dist/types.d.ts +52 -4
- package/dist/types.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/corpus/__snapshots__/corpus.test.ts.snap +269 -0
- package/src/index.ts +1 -0
- package/src/plan.ts +62 -9
- package/src/policy.ts +10 -3
- package/src/react/components.tsx +6 -2
- package/src/react/guuey-chat.tsx +103 -12
- package/src/react/structural-identity.ts +47 -0
- package/src/react/use-transcript.ts +5 -2
- package/src/strings.ts +6 -0
- package/src/types.ts +53 -4
- package/styles.css +10 -0
package/src/react/guuey-chat.tsx
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
forwardRef,
|
|
49
49
|
useCallback,
|
|
50
50
|
useEffect,
|
|
51
|
+
useId,
|
|
51
52
|
useImperativeHandle,
|
|
52
53
|
useMemo,
|
|
53
54
|
useRef,
|
|
@@ -64,9 +65,17 @@ import type { AgHitlAnswer, AgPausedAsk } from "@silverprotocol/core";
|
|
|
64
65
|
import { useAgentInvoke } from "@guuey/agent-client/react";
|
|
65
66
|
import type { UiResourceReader } from "@guuey/mcp-apps-host";
|
|
66
67
|
import { calmPolicy, debugPolicy, type TranscriptPolicy } from "../policy.js";
|
|
68
|
+
import { useStructuralIdentity } from "./structural-identity.js";
|
|
67
69
|
import { defaultChatStrings, type ChatStrings } from "../strings.js";
|
|
68
70
|
import { DEFAULT_CHAT_THEME, type GuueyChatTheme } from "../theme.js";
|
|
69
|
-
import type {
|
|
71
|
+
import type {
|
|
72
|
+
ChatDebugEvent,
|
|
73
|
+
ErrorItem,
|
|
74
|
+
PlanViewSummary,
|
|
75
|
+
PromptItem,
|
|
76
|
+
UserMessageItem,
|
|
77
|
+
ViewRefItem,
|
|
78
|
+
} from "../types.js";
|
|
70
79
|
import type { ThemeMode } from "./theme-css.js";
|
|
71
80
|
import { Transcript, type TranscriptWindowing } from "./transcript.js";
|
|
72
81
|
import type { TranscriptComponents, TranscriptItemContext } from "./components.js";
|
|
@@ -208,6 +217,19 @@ export interface GuueyChatProps {
|
|
|
208
217
|
* {@link GuueyChatHandle.threadId} for the pull-style read.
|
|
209
218
|
*/
|
|
210
219
|
onThread?: (threadId: string) => void;
|
|
220
|
+
/**
|
|
221
|
+
* guuey#301's host-stage trio (all optional; absent = today's inline
|
|
222
|
+
* behavior). `promotedViewKey` = the mount key the host's stage/canvas
|
|
223
|
+
* currently shows (chips it in the transcript — with
|
|
224
|
+
* `policy.view.presentation: "chips"` EVERY view chips and this key
|
|
225
|
+
* marks the selected one). `onViewRef` fires on chip click — set the
|
|
226
|
+
* key from it for the browser-history mechanic. `onViewsChange`
|
|
227
|
+
* delivers the plan's view roster (key/title/phase/channel/mount) so
|
|
228
|
+
* the host can render the selected mount with `<GuueyView>`.
|
|
229
|
+
*/
|
|
230
|
+
promotedViewKey?: string;
|
|
231
|
+
onViewRef?: (item: ViewRefItem) => void;
|
|
232
|
+
onViewsChange?: (views: PlanViewSummary[]) => void;
|
|
211
233
|
className?: string;
|
|
212
234
|
style?: CSSProperties;
|
|
213
235
|
}
|
|
@@ -239,6 +261,9 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
239
261
|
reader,
|
|
240
262
|
onDebugEvent,
|
|
241
263
|
viewProps,
|
|
264
|
+
promotedViewKey,
|
|
265
|
+
onViewRef,
|
|
266
|
+
onViewsChange,
|
|
242
267
|
onPromptAction,
|
|
243
268
|
onHitlAnswer,
|
|
244
269
|
oauthReturnTo,
|
|
@@ -250,15 +275,37 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
250
275
|
style,
|
|
251
276
|
} = props;
|
|
252
277
|
|
|
278
|
+
// Identity stabilization (guuey#303 QA — the template's own chat-rail
|
|
279
|
+
// shipped the failure): hosts pass inline literals and arrows, so prop
|
|
280
|
+
// IDENTITY is noise. The getter props route through refs (presence, not
|
|
281
|
+
// identity, is the re-mint trigger — flipping guest↔bearer is a real
|
|
282
|
+
// change; a fresh arrow per render is not), and the policy/strings
|
|
283
|
+
// overrides stabilize structurally below. Without this, every host
|
|
284
|
+
// re-render re-minted the plan, whose views-emission effect calls the
|
|
285
|
+
// host back → setState → re-render → "Maximum update depth exceeded".
|
|
286
|
+
const getAccessTokenRef = useRef(getAccessToken);
|
|
287
|
+
getAccessTokenRef.current = getAccessToken;
|
|
288
|
+
const getGuestSecretRef = useRef(getGuestSecret);
|
|
289
|
+
getGuestSecretRef.current = getGuestSecret;
|
|
290
|
+
const hasAccessToken = getAccessToken !== undefined;
|
|
291
|
+
const hasGuestSecret = getGuestSecret !== undefined;
|
|
292
|
+
|
|
253
293
|
const adapters = useMemo(
|
|
254
294
|
() =>
|
|
255
295
|
adaptersProp ??
|
|
256
296
|
createWebAdapters({
|
|
257
297
|
...(apiBaseUrl !== undefined ? { apiBaseUrl } : {}),
|
|
258
|
-
...(
|
|
259
|
-
|
|
298
|
+
...(hasAccessToken
|
|
299
|
+
? {
|
|
300
|
+
getAccessToken: (opts?: { forceRefresh?: boolean }) =>
|
|
301
|
+
getAccessTokenRef.current?.(opts) ?? Promise.resolve(null),
|
|
302
|
+
}
|
|
303
|
+
: {}),
|
|
304
|
+
...(hasGuestSecret
|
|
305
|
+
? { getGuestSecret: () => getGuestSecretRef.current?.() ?? null }
|
|
306
|
+
: {}),
|
|
260
307
|
}),
|
|
261
|
-
[adaptersProp, apiBaseUrl,
|
|
308
|
+
[adaptersProp, apiBaseUrl, hasAccessToken, hasGuestSecret],
|
|
262
309
|
);
|
|
263
310
|
const invoke = useAgentInvoke({ endpointUrl, ...(appId !== undefined ? { appId } : {}), adapters, preserveBlocks: true });
|
|
264
311
|
|
|
@@ -282,39 +329,80 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
282
329
|
// the guest secret is re-resolved each call so a rotation takes
|
|
283
330
|
// effect immediately — the same per-request property
|
|
284
331
|
// `createWebAdapters` documents for its own resolvers.
|
|
332
|
+
// Getters read through the refs at CALL time — the reader's identity
|
|
333
|
+
// survives a host re-render handing in fresh arrows, and a rotated
|
|
334
|
+
// getter takes effect on the next read (the same per-request property
|
|
335
|
+
// `createWebAdapters` documents).
|
|
336
|
+
const getToken = getAccessTokenRef.current;
|
|
337
|
+
const getGuest = getGuestSecretRef.current;
|
|
285
338
|
const read = createUiResourceReader({
|
|
286
339
|
apiBaseUrl,
|
|
287
340
|
threadId,
|
|
288
341
|
endpointUrl,
|
|
289
|
-
...(
|
|
290
|
-
guestSecret:
|
|
342
|
+
...(getToken !== undefined ? { getAccessToken: getToken } : {}),
|
|
343
|
+
guestSecret: getGuest !== undefined ? getGuest() : null,
|
|
291
344
|
});
|
|
292
345
|
return read(resourceUri);
|
|
293
346
|
};
|
|
294
|
-
}, [apiBaseUrl, endpointUrl
|
|
347
|
+
}, [apiBaseUrl, endpointUrl]);
|
|
295
348
|
const effectiveReader = reader ?? defaultReader;
|
|
296
349
|
|
|
350
|
+
// Structurally-stable overrides: `policy={{ view: { … } }}` inline
|
|
351
|
+
// literals keep ONE identity while their contents hold still, so the
|
|
352
|
+
// policy (and through it the plan) does not re-mint per host render.
|
|
353
|
+
const stablePolicyOverrides = useStructuralIdentity(policyOverrides);
|
|
354
|
+
const stableStringOverrides = useStructuralIdentity(stringOverrides);
|
|
297
355
|
const policy = useMemo(() => {
|
|
298
356
|
const factory = preset === "debug" ? debugPolicy : calmPolicy;
|
|
299
357
|
const strings: ChatStrings = {
|
|
300
358
|
...defaultChatStrings,
|
|
301
|
-
...
|
|
302
|
-
...
|
|
359
|
+
...stablePolicyOverrides?.strings,
|
|
360
|
+
...stableStringOverrides,
|
|
303
361
|
};
|
|
304
|
-
return factory({ ...
|
|
305
|
-
}, [preset,
|
|
362
|
+
return factory({ ...stablePolicyOverrides, strings });
|
|
363
|
+
}, [preset, stablePolicyOverrides, stableStringOverrides]);
|
|
306
364
|
|
|
307
365
|
const { inputs, resolvePrompt, answerHitlPrompt } = useTranscriptInputs(invoke);
|
|
366
|
+
// Memoized: once a chip is selected (`promotedViewKey` set) this object
|
|
367
|
+
// is on the plan's identity path — a per-render fresh spread here was the
|
|
368
|
+
// second leg of the render loop the template surfaced.
|
|
369
|
+
const transcriptInputs = useMemo(
|
|
370
|
+
() => (promotedViewKey !== undefined ? { ...inputs, promotedViewKey } : inputs),
|
|
371
|
+
[inputs, promotedViewKey],
|
|
372
|
+
);
|
|
308
373
|
const { plan, toggle, resolvedMounts, onViewPhase, onViewDiagnosis } = useTranscript({
|
|
309
|
-
inputs,
|
|
374
|
+
inputs: transcriptInputs,
|
|
310
375
|
policy,
|
|
311
376
|
...(effectiveReader !== undefined ? { reader: effectiveReader } : {}),
|
|
312
377
|
...(onDebugEvent !== undefined ? { onDebugEvent } : {}),
|
|
313
378
|
});
|
|
314
379
|
|
|
380
|
+
// guuey#301: hand the host the plan's view roster whenever it changes —
|
|
381
|
+
// the stage renders the selected mount from it. Locator entries carry
|
|
382
|
+
// the RESOLUTION overlay (a stage mounts material, not identities):
|
|
383
|
+
// resolved material replaces the locator mount, a failed read surfaces
|
|
384
|
+
// as the expired phase. Ref'd callback so a host passing a fresh
|
|
385
|
+
// closure each render doesn't loop the effect.
|
|
386
|
+
const onViewsChangeRef = useRef(onViewsChange);
|
|
387
|
+
onViewsChangeRef.current = onViewsChange;
|
|
388
|
+
useEffect(() => {
|
|
389
|
+
if (onViewsChangeRef.current === undefined) return;
|
|
390
|
+
const overlaid = plan.views.map((view) => {
|
|
391
|
+
const resolved = resolvedMounts.get(view.key);
|
|
392
|
+
if (resolved === undefined) return view;
|
|
393
|
+
if (resolved === "expired") return { ...view, phase: "expired" as const };
|
|
394
|
+
return { ...view, mount: resolved };
|
|
395
|
+
});
|
|
396
|
+
onViewsChangeRef.current(overlaid);
|
|
397
|
+
}, [plan.views, resolvedMounts]);
|
|
398
|
+
|
|
315
399
|
// ── Composer ─────────────────────────────────────────────────────────
|
|
316
400
|
const [input, setInput] = useState("");
|
|
317
401
|
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
|
402
|
+
// Browser form-field heuristics (a11y/autofill lints) flag a field with
|
|
403
|
+
// neither id nor name on every embedding site. useId keeps the id unique
|
|
404
|
+
// when several chats mount on one page — a static id would collide.
|
|
405
|
+
const composerId = useId();
|
|
318
406
|
const busy = invoke.status !== "ready";
|
|
319
407
|
const available = endpointUrl !== null;
|
|
320
408
|
const canSend = available && !busy && input.trim() !== "";
|
|
@@ -463,6 +551,7 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
463
551
|
resolvedMounts={resolvedMounts}
|
|
464
552
|
onViewPhase={onViewPhase}
|
|
465
553
|
onViewDiagnosis={onViewDiagnosis}
|
|
554
|
+
{...(onViewRef !== undefined ? { onViewRef } : {})}
|
|
466
555
|
{...(viewProps !== undefined ? { viewProps } : {})}
|
|
467
556
|
/>
|
|
468
557
|
{oauthReturn.notice !== null && (
|
|
@@ -487,6 +576,8 @@ export const GuueyChat = forwardRef<GuueyChatHandle, GuueyChatProps>(function Gu
|
|
|
487
576
|
>
|
|
488
577
|
<textarea
|
|
489
578
|
ref={inputRef}
|
|
579
|
+
id={composerId}
|
|
580
|
+
name="message"
|
|
490
581
|
className="guuey-chat-composer-input"
|
|
491
582
|
rows={1}
|
|
492
583
|
value={input}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Identity stabilization for host-supplied prop OBJECTS (guuey#303 QA).
|
|
3
|
+
*
|
|
4
|
+
* Hosts write `<GuueyChat policy={{ view: { presentation: "chips" } }}>` —
|
|
5
|
+
* a fresh object identity every render. Anything memo-keyed on that
|
|
6
|
+
* identity would re-mint per render, and anything the re-mint feeds into a
|
|
7
|
+
* `useEffect` → host `setState` edge becomes an infinite render loop (the
|
|
8
|
+
* template's chat-rail shipped exactly that). Identity is not a contract
|
|
9
|
+
* hosts signed up for; structure is.
|
|
10
|
+
*
|
|
11
|
+
* `useStructuralIdentity` returns the PREVIOUS reference while the new
|
|
12
|
+
* value is structurally equal, so downstream memos see one identity per
|
|
13
|
+
* structural value. Functions (and anything else non-plain) compare by
|
|
14
|
+
* reference — a policy override carrying an inline closure (for example
|
|
15
|
+
* `strings.humanizeTitle`) still churns; hoist such overrides.
|
|
16
|
+
*/
|
|
17
|
+
import { useRef } from "react";
|
|
18
|
+
|
|
19
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
20
|
+
if (typeof value !== "object" || value === null) return false;
|
|
21
|
+
const proto: unknown = Object.getPrototypeOf(value);
|
|
22
|
+
return proto === Object.prototype || proto === null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function structurallyEqual(a: unknown, b: unknown): boolean {
|
|
26
|
+
if (Object.is(a, b)) return true;
|
|
27
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
28
|
+
return a.length === b.length && a.every((item, i) => structurallyEqual(item, b[i]));
|
|
29
|
+
}
|
|
30
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
31
|
+
const aKeys = Object.keys(a);
|
|
32
|
+
const bKeys = Object.keys(b);
|
|
33
|
+
return (
|
|
34
|
+
aKeys.length === bKeys.length &&
|
|
35
|
+
aKeys.every((key) => Object.hasOwn(b, key) && structurallyEqual(a[key], b[key]))
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
// Functions, class instances, Maps, … — reference identity only (already
|
|
39
|
+
// handled by Object.is above).
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useStructuralIdentity<T>(value: T): T {
|
|
44
|
+
const ref = useRef(value);
|
|
45
|
+
if (!structurallyEqual(ref.current, value)) ref.current = value;
|
|
46
|
+
return ref.current;
|
|
47
|
+
}
|
|
@@ -174,12 +174,15 @@ export function useTranscript({
|
|
|
174
174
|
}, [plan]);
|
|
175
175
|
|
|
176
176
|
// Locator resolution — one read per locator key, misses become "expired".
|
|
177
|
+
// Walks the plan's VIEWS roster, not the display items: under the chips
|
|
178
|
+
// presentation (guuey#301) no inline mount renders, but the host's canvas
|
|
179
|
+
// still needs every locator resolved — the roster is presentation-
|
|
180
|
+
// independent, so resolution is too.
|
|
177
181
|
const readerRef = useRef(reader);
|
|
178
182
|
readerRef.current = reader;
|
|
179
183
|
const inFlight = useRef(new Set<ItemKey>());
|
|
180
184
|
useEffect(() => {
|
|
181
|
-
for (const item of plan.
|
|
182
|
-
if (item.kind !== "view") continue;
|
|
185
|
+
for (const item of plan.views) {
|
|
183
186
|
if (item.mount === null || item.mount.channel !== "locator") continue;
|
|
184
187
|
if (resolvedMounts.has(item.key) || inFlight.current.has(item.key)) continue;
|
|
185
188
|
const read = readerRef.current;
|
package/src/strings.ts
CHANGED
|
@@ -68,6 +68,10 @@ export interface ChatStrings {
|
|
|
68
68
|
}) => string;
|
|
69
69
|
/** guuey#204: the chip text for a mount promoted to a host stage/canvas. */
|
|
70
70
|
viewPromoted: (title: string) => string;
|
|
71
|
+
/** guuey#301 chips presentation: an unselected, mountable view's chip text. */
|
|
72
|
+
viewChip: (title: string) => string;
|
|
73
|
+
/** guuey#301 chips presentation: an expired/dead view's chip text (honest state). */
|
|
74
|
+
viewChipExpired: (title: string) => string;
|
|
71
75
|
/** Chip title when the mount has no producing-call title (history cards). */
|
|
72
76
|
viewRefFallbackTitle: string;
|
|
73
77
|
|
|
@@ -160,6 +164,8 @@ export const defaultChatStrings: ChatStrings = {
|
|
|
160
164
|
viewCspBlocked: (d) =>
|
|
161
165
|
`This page's Content-Security-Policy blocks ${d.blockedUri} — add "${d.violatedDirective} ${d.suggestedEntry}" to the policy so the view can start`,
|
|
162
166
|
viewPromoted: (title) => `${title} — on canvas`,
|
|
167
|
+
viewChip: (title) => title,
|
|
168
|
+
viewChipExpired: (title) => `${title} — expired`,
|
|
163
169
|
viewRefFallbackTitle: "Card",
|
|
164
170
|
|
|
165
171
|
recoveredFromHistory: "recovered from history",
|
package/src/types.ts
CHANGED
|
@@ -152,9 +152,11 @@ export interface TranscriptInputs {
|
|
|
152
152
|
* {@link ViewMountItem}, so an interactive surface exists exactly ONCE.
|
|
153
153
|
* Absent (or matching nothing / an expired mount) = today's behavior —
|
|
154
154
|
* every mount renders inline. Hosts derive the key with
|
|
155
|
-
* {@link newestViewKey} rather than hand-building it
|
|
156
|
-
*
|
|
157
|
-
*
|
|
155
|
+
* {@link newestViewKey} rather than hand-building it. Set per host
|
|
156
|
+
* click (via `onViewRef`) this is the SELECTION half of guuey#301's
|
|
157
|
+
* browser-history mechanic; the collapse-ALL-views half is the policy
|
|
158
|
+
* knob `view.presentation: "chips"` — the two compose, this field alone
|
|
159
|
+
* never chips more than the one promoted view.
|
|
158
160
|
*/
|
|
159
161
|
promotedViewKey?: string;
|
|
160
162
|
/** The last turn ended by user abort (R1 aborted-partial + "Stopped."). */
|
|
@@ -294,8 +296,23 @@ export interface ViewRefItem extends BaseItem {
|
|
|
294
296
|
kind: "viewRef";
|
|
295
297
|
/** The display title (the producing call's, or the strings fallback). */
|
|
296
298
|
title: string;
|
|
297
|
-
/**
|
|
299
|
+
/**
|
|
300
|
+
* The full resolved chip text — `strings.viewPromoted(title)` for the
|
|
301
|
+
* selected chip, `strings.viewChip(title)` / `viewChipExpired(title)`
|
|
302
|
+
* for the rest under chips presentation (guuey#301).
|
|
303
|
+
*/
|
|
298
304
|
label: string;
|
|
305
|
+
/**
|
|
306
|
+
* True when this chip's mount is the one the host's stage currently
|
|
307
|
+
* shows (`key === promotedViewKey` and mountable). Renderers style it
|
|
308
|
+
* as the active history entry (guuey#301).
|
|
309
|
+
*/
|
|
310
|
+
selected: boolean;
|
|
311
|
+
/**
|
|
312
|
+
* The underlying mount's phase — chips presentation keeps expired /
|
|
313
|
+
* unresolved views honest instead of hiding their state (guuey#301).
|
|
314
|
+
*/
|
|
315
|
+
phase: ViewHostPhase | "expired";
|
|
299
316
|
}
|
|
300
317
|
|
|
301
318
|
/** R7 — media blocks. */
|
|
@@ -488,6 +505,32 @@ export type ChatDebugEvent =
|
|
|
488
505
|
| { type: "unknown-block"; key: ItemKey; typeName: string; byteSize: number }
|
|
489
506
|
| { type: "turn-recovered"; marker: string };
|
|
490
507
|
|
|
508
|
+
/**
|
|
509
|
+
* One renderable view the plan saw, BEFORE any chips/promotion pass —
|
|
510
|
+
* the host-canvas contract (guuey#301): everything a stage needs to
|
|
511
|
+
* render the selected mount and label a history rail, in transcript
|
|
512
|
+
* order (history cards first, live mounts after — the same order the
|
|
513
|
+
* items carry).
|
|
514
|
+
*/
|
|
515
|
+
export interface PlanViewSummary {
|
|
516
|
+
key: ItemKey;
|
|
517
|
+
/** The display title (the producing call's, or the strings fallback). */
|
|
518
|
+
title: string;
|
|
519
|
+
phase: ViewHostPhase | "expired";
|
|
520
|
+
channel: ViewMountChannel | null;
|
|
521
|
+
/** Null when the locator is dead (the R13 expired path). */
|
|
522
|
+
mount: ViewMount | null;
|
|
523
|
+
/** The persisted `ui://` locator actions bind to (guuey#158), or null. */
|
|
524
|
+
actionScope: string | null;
|
|
525
|
+
/**
|
|
526
|
+
* Provenance: a LIVE fold mount vs a persisted HISTORY card. The roster
|
|
527
|
+
* is in transcript order (history cards sit with the settled prefix), so
|
|
528
|
+
* "newest" recency needs this — live outranks history, exactly
|
|
529
|
+
* {@link newestViewKey}'s walk.
|
|
530
|
+
*/
|
|
531
|
+
origin: "live" | "history";
|
|
532
|
+
}
|
|
533
|
+
|
|
491
534
|
export interface TranscriptPlan {
|
|
492
535
|
/** Ordered, stable keys (spec §7's determinism contract). */
|
|
493
536
|
items: DisplayItem[];
|
|
@@ -499,4 +542,10 @@ export interface TranscriptPlan {
|
|
|
499
542
|
* byte-identical to a streamed turn's (fixture 17).
|
|
500
543
|
*/
|
|
501
544
|
recovery: string | null;
|
|
545
|
+
/**
|
|
546
|
+
* Every view the plan saw (guuey#301's host-canvas contract) — present
|
|
547
|
+
* regardless of `view.presentation`, so a stage can render the selected
|
|
548
|
+
* mount even when the transcript shows only chips.
|
|
549
|
+
*/
|
|
550
|
+
views: PlanViewSummary[];
|
|
502
551
|
}
|
package/styles.css
CHANGED
|
@@ -262,6 +262,16 @@
|
|
|
262
262
|
color: var(--guuey-chat-ink);
|
|
263
263
|
border-color: color-mix(in srgb, var(--guuey-chat-ink) 24%, transparent);
|
|
264
264
|
}
|
|
265
|
+
/* guuey#301 chips presentation: the selected (on-stage) chip + honest expired state. */
|
|
266
|
+
.guuey-chat-view-ref-selected {
|
|
267
|
+
border-color: var(--guuey-chat-accent);
|
|
268
|
+
color: var(--guuey-chat-ink);
|
|
269
|
+
background: color-mix(in srgb, var(--guuey-chat-accent) 12%, var(--guuey-chat-surface));
|
|
270
|
+
}
|
|
271
|
+
.guuey-chat-view-ref-expired {
|
|
272
|
+
opacity: 0.6;
|
|
273
|
+
text-decoration: line-through;
|
|
274
|
+
}
|
|
265
275
|
|
|
266
276
|
/* ── R7 media ── */
|
|
267
277
|
.guuey-chat-media-image img {
|