@guuey/chat 0.10.0 → 0.12.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.
@@ -833,6 +833,7 @@ exports[`corpus > 1. forty-tools — grouping caps the rows; plan stays O(groups
833
833
  ],
834
834
  "recovery": null,
835
835
  "status": null,
836
+ "views": [],
836
837
  }
837
838
  `;
838
839
 
@@ -969,6 +970,7 @@ exports[`corpus > 2. midstream-tool-failure — failed inline; badge without unr
969
970
  ],
970
971
  "recovery": null,
971
972
  "status": null,
973
+ "views": [],
972
974
  }
973
975
  `;
974
976
 
@@ -992,6 +994,7 @@ exports[`corpus > 3. cold-start — R12 escalation at 0 / 2.5 s / 15 s 1`] = `
992
994
  "kind": "status",
993
995
  "state": "starting",
994
996
  },
997
+ "views": [],
995
998
  }
996
999
  `;
997
1000
 
@@ -1065,6 +1068,7 @@ exports[`corpus > 4. consent-gate — pending grant-mode card → answered one-l
1065
1068
  ],
1066
1069
  "recovery": null,
1067
1070
  "status": null,
1071
+ "views": [],
1068
1072
  }
1069
1073
  `;
1070
1074
 
@@ -1091,6 +1095,7 @@ exports[`corpus > 5. bypass-text-only — plan identical to a silver text-only s
1091
1095
  ],
1092
1096
  "recovery": null,
1093
1097
  "status": null,
1098
+ "views": [],
1094
1099
  }
1095
1100
  `;
1096
1101
 
@@ -1168,6 +1173,36 @@ exports[`corpus > 7. view-never-handshakes — channel-aware labels on both chan
1168
1173
  ],
1169
1174
  "recovery": null,
1170
1175
  "status": null,
1176
+ "views": [
1177
+ {
1178
+ "actionScope": null,
1179
+ "channel": "inline",
1180
+ "key": "view.tInline",
1181
+ "mount": {
1182
+ "channel": "inline",
1183
+ "resource": {
1184
+ "mimeType": "text/html",
1185
+ "text": "<p>form</p>",
1186
+ "uri": "ui://forms/1",
1187
+ },
1188
+ },
1189
+ "origin": "live",
1190
+ "phase": "no-handshake",
1191
+ "title": "render form",
1192
+ },
1193
+ {
1194
+ "actionScope": "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee",
1195
+ "channel": "locator",
1196
+ "key": "view.tGgui",
1197
+ "mount": {
1198
+ "channel": "locator",
1199
+ "resourceUri": "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee",
1200
+ },
1201
+ "origin": "live",
1202
+ "phase": "no-handshake",
1203
+ "title": "ggui render",
1204
+ },
1205
+ ],
1171
1206
  }
1172
1207
  `;
1173
1208
 
@@ -1212,6 +1247,7 @@ exports[`corpus > 8. aborted-mid-stream — partial kept + Stopped.; no orphaned
1212
1247
  "kind": "status",
1213
1248
  "state": "aborted",
1214
1249
  },
1250
+ "views": [],
1215
1251
  }
1216
1252
  `;
1217
1253
 
@@ -1267,6 +1303,29 @@ exports[`corpus > 9. history-dead-locators — the expired path beside a healthy
1267
1303
  ],
1268
1304
  "recovery": null,
1269
1305
  "status": null,
1306
+ "views": [
1307
+ {
1308
+ "actionScope": "ui://ggui/render/old/1",
1309
+ "channel": "locator",
1310
+ "key": "card.1",
1311
+ "mount": {
1312
+ "channel": "locator",
1313
+ "resourceUri": "ui://ggui/render/old/1",
1314
+ },
1315
+ "origin": "history",
1316
+ "phase": "negotiating",
1317
+ "title": "Card",
1318
+ },
1319
+ {
1320
+ "actionScope": null,
1321
+ "channel": null,
1322
+ "key": "card.2",
1323
+ "mount": null,
1324
+ "origin": "history",
1325
+ "phase": "expired",
1326
+ "title": "Card",
1327
+ },
1328
+ ],
1270
1329
  }
1271
1330
  `;
1272
1331
 
@@ -1284,6 +1343,7 @@ exports[`corpus > 10. empty-turn — status resolves, no empty bubble 1`] = `
1284
1343
  ],
1285
1344
  "recovery": null,
1286
1345
  "status": null,
1346
+ "views": [],
1287
1347
  }
1288
1348
  `;
1289
1349
 
@@ -1328,6 +1388,7 @@ exports[`corpus > 11. unknown-block-storm — labeled rows; nothing blank, nothi
1328
1388
  ],
1329
1389
  "recovery": null,
1330
1390
  "status": null,
1391
+ "views": [],
1331
1392
  }
1332
1393
  `;
1333
1394
 
@@ -1392,6 +1453,7 @@ exports[`corpus > 12. interleaved-media-code-citations — ordering and aggregat
1392
1453
  ],
1393
1454
  "recovery": null,
1394
1455
  "status": null,
1456
+ "views": [],
1395
1457
  }
1396
1458
  `;
1397
1459
 
@@ -1418,6 +1480,7 @@ exports[`corpus > 13. saturated-then-served — only R12 waiting, then a normal
1418
1480
  ],
1419
1481
  "recovery": null,
1420
1482
  "status": null,
1483
+ "views": [],
1421
1484
  }
1422
1485
  `;
1423
1486
 
@@ -1452,6 +1515,7 @@ exports[`corpus > 14. reasoning-heavy — calm collapses to one line; debug expa
1452
1515
  ],
1453
1516
  "recovery": null,
1454
1517
  "status": null,
1518
+ "views": [],
1455
1519
  }
1456
1520
  `;
1457
1521
 
@@ -1601,6 +1665,24 @@ exports[`corpus > 15. tools-around-a-view — the group SPLITS at the view's pos
1601
1665
  ],
1602
1666
  "recovery": null,
1603
1667
  "status": null,
1668
+ "views": [
1669
+ {
1670
+ "actionScope": null,
1671
+ "channel": "inline",
1672
+ "key": "view.t3",
1673
+ "mount": {
1674
+ "channel": "inline",
1675
+ "resource": {
1676
+ "mimeType": "text/html",
1677
+ "text": "<div>picker</div>",
1678
+ "uri": "ui://cards/slot-picker",
1679
+ },
1680
+ },
1681
+ "origin": "live",
1682
+ "phase": "negotiating",
1683
+ "title": "ggui render",
1684
+ },
1685
+ ],
1604
1686
  }
1605
1687
  `;
1606
1688
 
@@ -1618,6 +1700,7 @@ exports[`corpus > 16. user-send-failure — R0 failed with retry; the message ne
1618
1700
  ],
1619
1701
  "recovery": null,
1620
1702
  "status": null,
1703
+ "views": [],
1621
1704
  }
1622
1705
  `;
1623
1706
 
@@ -1739,6 +1822,32 @@ exports[`corpus > 19. persisted-plus-live — the fold owns its trailing turns;
1739
1822
  "kind": "status",
1740
1823
  "state": "using-tool",
1741
1824
  },
1825
+ "views": [
1826
+ {
1827
+ "actionScope": "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee",
1828
+ "channel": "locator",
1829
+ "key": "view.tA",
1830
+ "mount": {
1831
+ "channel": "locator",
1832
+ "resourceUri": "ui://ggui/render/render_00000000-0000-4000-8000-300000000001/c0ffee",
1833
+ },
1834
+ "origin": "live",
1835
+ "phase": "negotiating",
1836
+ "title": "ggui render",
1837
+ },
1838
+ {
1839
+ "actionScope": "ui://ggui/render/old/1",
1840
+ "channel": "locator",
1841
+ "key": "card.1",
1842
+ "mount": {
1843
+ "channel": "locator",
1844
+ "resourceUri": "ui://ggui/render/old/1",
1845
+ },
1846
+ "origin": "history",
1847
+ "phase": "negotiating",
1848
+ "title": "Card",
1849
+ },
1850
+ ],
1742
1851
  }
1743
1852
  `;
1744
1853
 
@@ -1807,6 +1916,7 @@ exports[`corpus > 20. hitl-grant-modes — the persisted declaration renders; an
1807
1916
  ],
1808
1917
  "recovery": null,
1809
1918
  "status": null,
1919
+ "views": [],
1810
1920
  }
1811
1921
  `;
1812
1922
 
@@ -1849,6 +1959,7 @@ exports[`corpus > 21. notice-rows — both arrival paths render labeled, never a
1849
1959
  ],
1850
1960
  "recovery": null,
1851
1961
  "status": null,
1962
+ "views": [],
1852
1963
  }
1853
1964
  `;
1854
1965
 
@@ -1912,11 +2023,47 @@ exports[`corpus > 22. promoted-view — the stage's mount chips (guuey#204); oth
1912
2023
  "key": "view.t2",
1913
2024
  "kind": "viewRef",
1914
2025
  "label": "ggui render — on canvas",
2026
+ "phase": "negotiating",
2027
+ "selected": true,
1915
2028
  "title": "ggui render",
1916
2029
  },
1917
2030
  ],
1918
2031
  "recovery": null,
1919
2032
  "status": null,
2033
+ "views": [
2034
+ {
2035
+ "actionScope": null,
2036
+ "channel": "inline",
2037
+ "key": "view.t1",
2038
+ "mount": {
2039
+ "channel": "inline",
2040
+ "resource": {
2041
+ "mimeType": "text/html",
2042
+ "text": "<div>t1</div>",
2043
+ "uri": "ui://cards/t1",
2044
+ },
2045
+ },
2046
+ "origin": "live",
2047
+ "phase": "negotiating",
2048
+ "title": "ggui render",
2049
+ },
2050
+ {
2051
+ "actionScope": null,
2052
+ "channel": "inline",
2053
+ "key": "view.t2",
2054
+ "mount": {
2055
+ "channel": "inline",
2056
+ "resource": {
2057
+ "mimeType": "text/html",
2058
+ "text": "<div>t2</div>",
2059
+ "uri": "ui://cards/t2",
2060
+ },
2061
+ },
2062
+ "origin": "live",
2063
+ "phase": "negotiating",
2064
+ "title": "ggui render",
2065
+ },
2066
+ ],
1920
2067
  }
1921
2068
  `;
1922
2069
 
@@ -1962,6 +2109,20 @@ exports[`corpus > 23. meta-less-locator — a \`_meta\`-withholding producer's r
1962
2109
  ],
1963
2110
  "recovery": null,
1964
2111
  "status": null,
2112
+ "views": [
2113
+ {
2114
+ "actionScope": "ui://ggui/render/render_dark/h1",
2115
+ "channel": "locator",
2116
+ "key": "view.t1",
2117
+ "mount": {
2118
+ "channel": "locator",
2119
+ "resourceUri": "ui://ggui/render/render_dark/h1",
2120
+ },
2121
+ "origin": "live",
2122
+ "phase": "negotiating",
2123
+ "title": "ggui render",
2124
+ },
2125
+ ],
1965
2126
  }
1966
2127
  `;
1967
2128
 
@@ -2016,6 +2177,113 @@ exports[`corpus > 24. prod-wire-ggui-render — ggui's production posture: no _m
2016
2177
  ],
2017
2178
  "recovery": null,
2018
2179
  "status": null,
2180
+ "views": [
2181
+ {
2182
+ "actionScope": "ui://ggui/render/render_5d11b1b4-75ba-44e5-81bb-0b420b86bb8e/7328555b44e72bd7",
2183
+ "channel": "locator",
2184
+ "key": "view.t1",
2185
+ "mount": {
2186
+ "channel": "locator",
2187
+ "resourceUri": "ui://ggui/render/render_5d11b1b4-75ba-44e5-81bb-0b420b86bb8e/7328555b44e72bd7",
2188
+ },
2189
+ "origin": "live",
2190
+ "phase": "negotiating",
2191
+ "title": "ggui render",
2192
+ },
2193
+ ],
2194
+ }
2195
+ `;
2196
+
2197
+ exports[`corpus > 25. chips-presentation — EVERY view chips; the promoted key marks the selection; views roster carries the mounts (guuey#301) 1`] = `
2198
+ {
2199
+ "items": [
2200
+ {
2201
+ "expanded": true,
2202
+ "key": "u0",
2203
+ "kind": "user",
2204
+ "retry": false,
2205
+ "state": "sent",
2206
+ "text": "show me two cards",
2207
+ },
2208
+ {
2209
+ "argsPreview": null,
2210
+ "attribution": true,
2211
+ "expanded": false,
2212
+ "key": "tool.t1",
2213
+ "kind": "tool",
2214
+ "name": "ggui_render",
2215
+ "result": null,
2216
+ "state": "done",
2217
+ "title": "ggui render",
2218
+ "toolCallId": "t1",
2219
+ },
2220
+ {
2221
+ "expanded": false,
2222
+ "key": "view.t1",
2223
+ "kind": "viewRef",
2224
+ "label": "ggui render",
2225
+ "phase": "negotiating",
2226
+ "selected": false,
2227
+ "title": "ggui render",
2228
+ },
2229
+ {
2230
+ "argsPreview": null,
2231
+ "attribution": true,
2232
+ "expanded": false,
2233
+ "key": "tool.t2",
2234
+ "kind": "tool",
2235
+ "name": "ggui_render",
2236
+ "result": null,
2237
+ "state": "done",
2238
+ "title": "ggui render",
2239
+ "toolCallId": "t2",
2240
+ },
2241
+ {
2242
+ "expanded": false,
2243
+ "key": "view.t2",
2244
+ "kind": "viewRef",
2245
+ "label": "ggui render — on canvas",
2246
+ "phase": "negotiating",
2247
+ "selected": true,
2248
+ "title": "ggui render",
2249
+ },
2250
+ ],
2251
+ "recovery": null,
2252
+ "status": null,
2253
+ "views": [
2254
+ {
2255
+ "actionScope": null,
2256
+ "channel": "inline",
2257
+ "key": "view.t1",
2258
+ "mount": {
2259
+ "channel": "inline",
2260
+ "resource": {
2261
+ "mimeType": "text/html",
2262
+ "text": "<div>t1</div>",
2263
+ "uri": "ui://cards/t1",
2264
+ },
2265
+ },
2266
+ "origin": "live",
2267
+ "phase": "negotiating",
2268
+ "title": "ggui render",
2269
+ },
2270
+ {
2271
+ "actionScope": null,
2272
+ "channel": "inline",
2273
+ "key": "view.t2",
2274
+ "mount": {
2275
+ "channel": "inline",
2276
+ "resource": {
2277
+ "mimeType": "text/html",
2278
+ "text": "<div>t2</div>",
2279
+ "uri": "ui://cards/t2",
2280
+ },
2281
+ },
2282
+ "origin": "live",
2283
+ "phase": "negotiating",
2284
+ "title": "ggui render",
2285
+ },
2286
+ ],
2019
2287
  }
2020
2288
  `;
2021
2289
 
@@ -2098,5 +2366,6 @@ exports[`corpus > 25. oauth-auth-ask — kind:auth + authConfig oauth2 plans as
2098
2366
  ],
2099
2367
  "recovery": null,
2100
2368
  "status": null,
2369
+ "views": [],
2101
2370
  }
2102
2371
  `;
package/src/index.ts CHANGED
@@ -77,6 +77,7 @@ export type {
77
77
  TranscriptMessage,
78
78
  TranscriptOverrides,
79
79
  TranscriptPlan,
80
+ PlanViewSummary,
80
81
  UnknownItem,
81
82
  UserMessageItem,
82
83
  ViewMountItem,
package/src/plan.ts CHANGED
@@ -48,6 +48,7 @@ import type {
48
48
  DisplayItem,
49
49
  ItemKey,
50
50
  NoticeItem,
51
+ PlanViewSummary,
51
52
  StatusLineItem,
52
53
  ToolGroupItem,
53
54
  ToolItem,
@@ -148,17 +149,43 @@ function foldAssistantSources(
148
149
  const hasTurnIdentity = result.messages.some(
149
150
  (m) => m.role === "assistant" && m.turnId !== undefined,
150
151
  );
152
+ // ONE source per conversational TURN, not per assistant message segment
153
+ // (guuey#306 — the mis-slotting bug): a pod fold emits SEVERAL assistant
154
+ // messages per turn (thought/text/tool interleave — a generative-card
155
+ // turn is ~6-8 segments), and the conversation loop pairs users to
156
+ // sources BY SLOT — per-segment sources stretched the index space, so
157
+ // from turn 2 on every user bubble rendered clustered inside turn 1's
158
+ // segment run. Grouping: a `user` row closes the current group (the
159
+ // fold's messages are the whole conversation, users included), and a
160
+ // turnId CHANGE between assistant segments splits too (identity beats
161
+ // adjacency — two turns without an intervening user row still separate).
151
162
  const sources: AssistantSource[] = [];
152
163
  const notices: FoldNotice[] = [];
164
+ let current: AssistantSource | null = null;
165
+ let currentTurnId: string | undefined;
153
166
  for (const m of result.messages) {
154
- if (m.role === "assistant") {
155
- sources.push({
156
- blocks: [...m.content],
157
- live: inFlight && hasTurnIdentity && m.turnId !== undefined && openTurns.has(m.turnId),
158
- stopped: false,
159
- });
167
+ if (m.role === "user") {
168
+ current = null;
169
+ currentTurnId = undefined;
170
+ } else if (m.role === "assistant") {
171
+ const startsNewTurn =
172
+ current === null ||
173
+ (m.turnId !== undefined && currentTurnId !== undefined && m.turnId !== currentTurnId);
174
+ if (startsNewTurn) {
175
+ current = { blocks: [], live: false, stopped: false };
176
+ sources.push(current);
177
+ currentTurnId = undefined;
178
+ }
179
+ if (m.turnId !== undefined) currentTurnId = m.turnId;
180
+ current!.blocks.push(...m.content);
181
+ if (inFlight && hasTurnIdentity && m.turnId !== undefined && openTurns.has(m.turnId)) {
182
+ current!.live = true;
183
+ }
160
184
  } else if (m.role === "tool" && sources.length > 0) {
161
- sources[sources.length - 1]!.blocks.push(...m.content);
185
+ // Tool results belong to the turn in progress — or, when they trail a
186
+ // user boundary before the next assistant segment, to the PRECEDING
187
+ // turn (the pre-#306 last-source semantics, kept).
188
+ (current ?? sources[sources.length - 1]!).blocks.push(...m.content);
162
189
  } else if (m.role === "notice") {
163
190
  // R16 (spec draft.2): a session annotation — a PEER row, never folded
164
191
  // into assistant content; it anchors after the source it followed.
@@ -177,10 +204,33 @@ function foldAssistantSources(
177
204
  return { sources, notices };
178
205
  }
179
206
 
207
+ /**
208
+ * Flat assistant rows grouped by conversational ORDER (guuey#306): a user
209
+ * row closes the current slot, consecutive assistant rows merge into one —
210
+ * the assembler's interleave IS the turn structure, and the old
211
+ * filter/zip threw it away (double/empty assistant rows drifted every
212
+ * later user bubble by one). Shared by the flat path and the fold-vs-flat
213
+ * seam, so both count the same slots.
214
+ */
215
+ function flatSettledGroups(messages: TranscriptInputs["messages"]): AssistantSource[] {
216
+ const groups: AssistantSource[] = [];
217
+ let current: AssistantSource | null = null;
218
+ for (const m of messages) {
219
+ if (m.role === "user") {
220
+ current = null;
221
+ } else if (m.role === "assistant") {
222
+ if (current === null) {
223
+ current = { blocks: [], live: false, stopped: false };
224
+ groups.push(current);
225
+ }
226
+ current.blocks.push({ type: "text", text: m.text });
227
+ }
228
+ }
229
+ return groups;
230
+ }
231
+
180
232
  function flatAssistantSources(inputs: TranscriptInputs, inFlight: boolean): AssistantSource[] {
181
- const settled: AssistantSource[] = inputs.messages
182
- .filter((m) => m.role === "assistant")
183
- .map((m) => ({ blocks: [{ type: "text", text: m.text }], live: false, stopped: false }));
233
+ const settled: AssistantSource[] = flatSettledGroups(inputs.messages);
184
234
  // The in-flight (or abort-kept) partial is its own trailing slot — settled
185
235
  // turns live in `messages`; `assistantText` is ignored once `ready` again
186
236
  // UNLESS the turn ended by abort (R1 aborted-partial keeps it).
@@ -726,29 +776,23 @@ export function planTranscript(
726
776
  // keep rendering as flat text slots in front. When the fold spans the
727
777
  // whole conversation (a pure-live session — the corpus's world), the kept
728
778
  // prefix is empty and the plan is byte-identical to the old wholesale
729
- // replace. Alignment is by COUNT (trailing flat entries ↔ settled fold
730
- // sources): flat rows carry no turn identity, so a persisted CARD-ONLY
731
- // turn inside the fold's span (an empty-text assistant row the assembler
732
- // dropped) can offset the seam by one a documented approximation, same
733
- // class as the header's read-plane-seq note, resolved for the visible
734
- // cards by the actionScope dedupe below.
779
+ // replace. Alignment counts TURNS on
780
+ // both sides (guuey#306): fold sources group per turn (user boundary +
781
+ // turnId change), flat entries group by conversational order the old
782
+ // per-message COUNT approximation (and its one-off seam drift on
783
+ // card-only turns) is gone.
735
784
  let assistants: AssistantSource[];
736
785
  if (inputs.result) {
737
786
  const fold = foldAssistantSources(inputs.result, inFlight, inputs.aborted === true);
738
787
  const foldSources = fold.sources;
739
788
  const settledFoldCount = foldSources.filter((s) => !s.live).length;
740
- const flatSettled = inputs.messages.filter((m) => m.role === "assistant");
741
- const keep = Math.max(0, flatSettled.length - settledFoldCount);
742
- assistants = [
743
- ...flatSettled.slice(0, keep).map(
744
- (m): AssistantSource => ({
745
- blocks: [{ type: "text", text: m.text }],
746
- live: false,
747
- stopped: false,
748
- }),
749
- ),
750
- ...foldSources,
751
- ];
789
+ // Both sides of the seam now count TURNS (guuey#306): fold sources are
790
+ // per-turn groups, and the flat prefix uses the SAME order-derived
791
+ // grouping — the old per-message counts were the documented
792
+ // approximation this replaces.
793
+ const flatGroups = flatSettledGroups(inputs.messages);
794
+ const keep = Math.max(0, flatGroups.length - settledFoldCount);
795
+ assistants = [...flatGroups.slice(0, keep), ...foldSources];
752
796
  if (policy.notice.show) {
753
797
  // Fold-borne notices anchor to the fold source they followed, which
754
798
  // sits at global slot `keep + afterSourceIndex` after the seam.
@@ -919,26 +963,77 @@ export function planTranscript(
919
963
  });
920
964
  }
921
965
 
922
- // guuey#204 "promote and reference": the ONE mount a host-owned stage
923
- // shows (`inputs.promotedViewKey`) renders as a compact reference chip
924
- // instead of a second live app instance. A key matching nothing (stale
925
- // host state) or an expired mount promotes nothing — the honest no-op;
926
- // the next render's key corrects it. The chip keeps the mount's key, so
927
- // overrides and phase reports survive a swap back to the full mount.
966
+ // The host-canvas contract (guuey#301): every view the plan saw, with
967
+ // its mount material collected BEFORE any chips/promotion pass, so a
968
+ // stage can render the selected mount even when the transcript shows
969
+ // only chips.
970
+ const views: PlanViewSummary[] = [];
971
+ for (const item of items) {
972
+ if (item.kind !== "view") continue;
973
+ views.push({
974
+ key: item.key,
975
+ title: item.toolTitle ?? policy.strings.viewRefFallbackTitle,
976
+ phase: item.phase,
977
+ channel: item.channel,
978
+ mount: item.mount,
979
+ actionScope: item.actionScope,
980
+ // The kit's own key scheme is authoritative here: `card.{seq}` is
981
+ // exactly the history-card emission (this file builds both).
982
+ origin: item.key.startsWith("card.") ? "history" : "live",
983
+ });
984
+ }
985
+
986
+ // Two composable behaviors over the assembled items:
987
+ //
988
+ // guuey#204 "promote and reference" (inline presentation): the ONE mount
989
+ // a host-owned stage shows (`inputs.promotedViewKey`) renders as a
990
+ // compact reference chip instead of a second live app instance. A key
991
+ // matching nothing (stale host state) or an expired mount promotes
992
+ // nothing — the honest no-op; the next render's key corrects it.
993
+ //
994
+ // guuey#301 chips presentation (`policy.view.presentation === "chips"`):
995
+ // EVERY view plans as a chip — the full render belongs to the host's
996
+ // stage; the promoted key marks the SELECTED chip (browser-history
997
+ // mechanic), and expired/dead views chip with their honest state.
998
+ //
999
+ // Either way a chip keeps the mount's key, so overrides and phase
1000
+ // reports survive a swap back to the full mount.
928
1001
  const promoted = inputs.promotedViewKey;
1002
+ const chips = policy.view.presentation === "chips";
929
1003
  const finalItems =
930
- promoted === undefined
1004
+ !chips && promoted === undefined
931
1005
  ? items
932
1006
  : items.map((item): DisplayItem => {
933
- if (item.kind !== "view" || item.key !== promoted) return item;
934
- if (item.phase === "expired" || item.mount === null) return item;
1007
+ if (item.kind !== "view") return item;
935
1008
  const title = item.toolTitle ?? policy.strings.viewRefFallbackTitle;
1009
+ if (chips) {
1010
+ const mountable = item.phase !== "expired" && item.mount !== null;
1011
+ const selected = mountable && item.key === promoted;
1012
+ const ref: ViewRefItem = {
1013
+ kind: "viewRef",
1014
+ key: item.key,
1015
+ expanded: false,
1016
+ title,
1017
+ label: selected
1018
+ ? policy.strings.viewPromoted(title)
1019
+ : mountable
1020
+ ? policy.strings.viewChip(title)
1021
+ : policy.strings.viewChipExpired(title),
1022
+ selected,
1023
+ phase: item.phase,
1024
+ };
1025
+ return ref;
1026
+ }
1027
+ if (item.key !== promoted) return item;
1028
+ if (item.phase === "expired" || item.mount === null) return item;
936
1029
  const ref: ViewRefItem = {
937
1030
  kind: "viewRef",
938
1031
  key: item.key,
939
1032
  expanded: false,
940
1033
  title,
941
1034
  label: policy.strings.viewPromoted(title),
1035
+ selected: true,
1036
+ phase: item.phase,
942
1037
  };
943
1038
  return ref;
944
1039
  });
@@ -948,6 +1043,7 @@ export function planTranscript(
948
1043
  status: deriveStatus(inputs, policy),
949
1044
  recovery:
950
1045
  inputs.adopted === true && policy.debugDetail ? policy.strings.recoveredFromHistory : null,
1046
+ views,
951
1047
  };
952
1048
  }
953
1049
 
package/src/policy.ts CHANGED
@@ -40,8 +40,15 @@ export interface TranscriptPolicy {
40
40
  /** Preview bound — the plan never carries a full giant payload (fixture 6). */
41
41
  previewChars: number;
42
42
  };
43
- /** R6 (sandbox overrides pass through to `<GuueyView>` in 3b). */
44
- view: { timeoutMs: number };
43
+ /**
44
+ * R6 (sandbox overrides pass through to `<GuueyView>` in 3b).
45
+ * `presentation` (guuey#301): `"inline"` = every mount renders as a live
46
+ * view in the transcript (the default, unchanged); `"chips"` = EVERY
47
+ * view plans as a compact {@link ViewRefItem} — the full render belongs
48
+ * to a host-owned stage fed by `TranscriptPlan.views` + the selection
49
+ * input `promotedViewKey`.
50
+ */
51
+ view: { timeoutMs: number; presentation: "inline" | "chips" };
45
52
  /** R7. */
46
53
  media: { inlineImageCapRem: number; chipOnly: boolean };
47
54
  /** R8. */
@@ -114,7 +121,7 @@ function calmBase(): TranscriptPolicy {
114
121
  tool: { expandByDefault: false, argsVisible: false, humanizeTitle: humanizeToolName },
115
122
  toolGroup: { threshold: 2 },
116
123
  dataResult: { capRem: 16, prettyPrint: true, alwaysShowBytes: false, previewChars: 2048 },
117
- view: { timeoutMs: 8000 },
124
+ view: { timeoutMs: 8000, presentation: "inline" },
118
125
  media: { inlineImageCapRem: 20, chipOnly: false },
119
126
  code: { capRem: 16, wrap: false },
120
127
  citations: { style: "chips" },