Skip to content

Commit 8c3d29b

Browse files
committed
Add presentation refs and snapshot compare flow
1 parent 3f116c9 commit 8c3d29b

37 files changed

Lines changed: 2066 additions & 222 deletions

docs/standards/CCCS_V1.md

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -292,20 +292,52 @@ type AttachmentRefV1 = {
292292
### 8.2 `ReferenceV1`
293293
294294
```ts
295-
type ReferenceV1 = {
296-
kind?: "file" | "url" | "commit" | "text" // default "url"
297-
url?: string
298-
path?: string
299-
title?: string
300-
sha?: string
301-
bytes?: number
302-
// extra fields MAY exist; clients MUST ignore unknown fields
303-
}
295+
type ReferenceV1 =
296+
| {
297+
kind?: "file" | "url" | "commit" | "text" // default "url"
298+
url?: string
299+
path?: string
300+
title?: string
301+
sha?: string
302+
bytes?: number
303+
// extra fields MAY exist; clients MUST ignore unknown fields
304+
}
305+
| {
306+
kind: "presentation_ref"
307+
v?: 1
308+
slot_id: string
309+
label?: string
310+
locator_label?: string
311+
title?: string
312+
card_type?: string
313+
status?: "open" | "needs_user" | "resolved"
314+
href?: string
315+
excerpt?: string
316+
locator?: Record<string, unknown>
317+
snapshot?: {
318+
path: string
319+
mime_type?: string
320+
bytes?: number
321+
sha256?: string
322+
width?: number
323+
height?: number
324+
captured_at?: string
325+
source?: "browser_surface" | "pdf_viewer" | "viewer_dom" | string
326+
}
327+
// extra fields MAY exist; clients MUST ignore unknown fields
328+
}
304329
```
305330
306331
**Rules**
307332
- Attachments SHOULD include content hashes where possible (`sha256`) to enable reproducibility/auditing.
308333
- `path` MUST be stable and retrievable within the group’s storage scope.
334+
- `kind="presentation_ref"` is a structured evidence anchor into a group Presentation slot.
335+
- `slot_id` MUST identify the target slot within the group presentation surface.
336+
- `locator` SHOULD carry best-effort position hints for the current view (for example page, heading, row, or scroll state).
337+
- `snapshot`, when present, SHOULD be treated as a best-effort visual anchor for the quoted view.
338+
- `snapshot.path` MUST be a stable group-scoped blob path.
339+
- Implementations SHOULD use `locator` for precise recovery when possible, and `snapshot` as a visual fallback when precise recovery is unavailable.
340+
- `status`, when present, is advisory metadata for the referenced discussion state; ledger-derived obligation status remains authoritative.
309341
310342
### 8.3 Attachment Resolution (Non‑Normative Guidance)
311343

src/cccc/daemon/messaging/chat_ops.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ def _build_delivery_text(
6868
priority: str,
6969
reply_required: bool,
7070
event_id: str,
71+
refs: list[dict[str, Any]],
7172
attachments: list[dict[str, Any]],
7273
src_group_id: str = "",
7374
src_event_id: str = "",
@@ -82,6 +83,9 @@ def _build_delivery_text(
8283
prefix_lines.append(f"[cccc] RELAYED FROM (group_id={src_group_id}, event_id={src_event_id}):")
8384
if prefix_lines:
8485
delivery_text = "\n".join(prefix_lines) + "\n" + delivery_text
86+
ref_lines = _render_delivery_refs(refs)
87+
if ref_lines:
88+
delivery_text = (delivery_text.rstrip("\n") + "\n\n" + "\n".join(ref_lines)).strip()
8589
if attachments:
8690
lines = ["[cccc] Attachments:"]
8791
for attachment in attachments[:8]:
@@ -95,6 +99,112 @@ def _build_delivery_text(
9599
return delivery_text
96100

97101

102+
def _compact_delivery_text(value: Any, *, limit: int) -> str:
103+
text = re.sub(r"\s+", " ", str(value or "").strip())
104+
if not text:
105+
return ""
106+
if len(text) <= limit:
107+
return text
108+
return text[: max(1, limit - 1)].rstrip() + "…"
109+
110+
111+
def _presentation_slot_label(slot_id: str, label: str) -> str:
112+
if label:
113+
return label
114+
match = re.search(r"(\d+)$", slot_id)
115+
if match:
116+
try:
117+
return f"P{int(match.group(1))}"
118+
except Exception:
119+
pass
120+
return slot_id or "Presentation"
121+
122+
123+
def _render_delivery_refs(refs: list[dict[str, Any]]) -> list[str]:
124+
if not refs:
125+
return []
126+
127+
lines = ["[cccc] References:"]
128+
rendered = 0
129+
130+
for ref in refs:
131+
if not isinstance(ref, dict):
132+
continue
133+
kind = str(ref.get("kind") or "").strip()
134+
if kind == "presentation_ref":
135+
slot_id = _compact_delivery_text(ref.get("slot_id"), limit=32)
136+
label = _presentation_slot_label(
137+
slot_id,
138+
_compact_delivery_text(ref.get("label"), limit=24),
139+
)
140+
locator_label = _compact_delivery_text(ref.get("locator_label"), limit=48)
141+
title = _compact_delivery_text(ref.get("title"), limit=72)
142+
header = f"- {label}"
143+
if slot_id:
144+
header += f" ({slot_id})"
145+
if locator_label:
146+
header += f" · {locator_label}"
147+
if title:
148+
header += f" — {title}"
149+
lines.append(header)
150+
excerpt = _compact_delivery_text(ref.get("excerpt"), limit=120)
151+
if excerpt:
152+
lines.append(f' excerpt: "{excerpt}"')
153+
href = _compact_delivery_text(ref.get("href"), limit=120)
154+
if href:
155+
lines.append(f" href: {href}")
156+
locator = ref.get("locator") if isinstance(ref.get("locator"), dict) else {}
157+
locator_url = _compact_delivery_text(locator.get("url"), limit=120)
158+
if locator_url and locator_url != href:
159+
lines.append(f" view_url: {locator_url}")
160+
captured_at = _compact_delivery_text(locator.get("captured_at"), limit=48)
161+
if captured_at:
162+
lines.append(f" captured_at: {captured_at}")
163+
viewer_scroll_top = locator.get("viewer_scroll_top")
164+
if isinstance(viewer_scroll_top, (int, float)) or str(viewer_scroll_top or "").strip():
165+
try:
166+
scroll_value = int(float(viewer_scroll_top))
167+
except Exception:
168+
scroll_value = None
169+
if scroll_value is not None and scroll_value >= 0:
170+
lines.append(f" scroll_top: {scroll_value}")
171+
snapshot = ref.get("snapshot") if isinstance(ref.get("snapshot"), dict) else {}
172+
snapshot_path = _compact_delivery_text(snapshot.get("path"), limit=120)
173+
if snapshot_path:
174+
width = snapshot.get("width")
175+
height = snapshot.get("height")
176+
size_label = ""
177+
try:
178+
width_value = int(width)
179+
height_value = int(height)
180+
if width_value > 0 and height_value > 0:
181+
size_label = f" ({width_value}x{height_value})"
182+
except Exception:
183+
size_label = ""
184+
lines.append(f" snapshot: {snapshot_path}{size_label}")
185+
rendered += 1
186+
if rendered >= 4:
187+
break
188+
continue
189+
190+
summary = _compact_delivery_text(
191+
ref.get("title") or ref.get("path") or ref.get("url") or kind,
192+
limit=96,
193+
)
194+
if summary:
195+
prefix = kind or "ref"
196+
lines.append(f"- {prefix}: {summary}")
197+
rendered += 1
198+
if rendered >= 4:
199+
break
200+
201+
if rendered == 0:
202+
return []
203+
if len(refs) > rendered:
204+
lines.append(f"- … ({len(refs) - rendered} more)")
205+
return lines
206+
207+
98208
def _touch_registry_updated_at(group_id: str, ts: str) -> None:
99209
try:
100210
reg = load_registry()
@@ -106,6 +216,16 @@ def _touch_registry_updated_at(group_id: str, ts: str) -> None:
106216
pass
107217

108218

219+
def _normalize_refs(raw: Any) -> list[dict[str, Any]]:
220+
if not isinstance(raw, list):
221+
return []
222+
refs: list[dict[str, Any]] = []
223+
for item in raw:
224+
if isinstance(item, dict):
225+
refs.append(item)
226+
return refs
227+
228+
109229
def _notify_headless_targets(
110230
*,
111231
group: Any,
@@ -269,6 +389,7 @@ def handle_send(
269389
attachments = normalize_attachments(group, args.get("attachments"))
270390
except Exception as e:
271391
return _error("invalid_attachments", str(e))
392+
refs = _normalize_refs(args.get("refs"))
272393

273394
if not text.strip() and not attachments:
274395
return _error("empty_message", "message text cannot be empty")
@@ -285,6 +406,7 @@ def handle_send(
285406
priority=priority,
286407
reply_required=reply_required,
287408
to=to,
409+
refs=refs,
288410
attachments=attachments,
289411
source_platform=source_platform or None,
290412
source_user_name=source_user_name or None,
@@ -307,6 +429,7 @@ def handle_send(
307429
priority=priority,
308430
reply_required=reply_required,
309431
event_id=event_id,
432+
refs=refs,
310433
attachments=attachments,
311434
src_group_id=src_group_id,
312435
src_event_id=src_event_id,
@@ -447,6 +570,7 @@ def handle_reply(
447570
attachments = normalize_attachments(group, args.get("attachments"))
448571
except Exception as e:
449572
return _error("invalid_attachments", str(e))
573+
refs = _normalize_refs(args.get("refs"))
450574
if not text.strip() and not attachments:
451575
return _error("empty_message", "message text cannot be empty")
452576

@@ -464,6 +588,7 @@ def handle_reply(
464588
to=to,
465589
reply_to=reply_to,
466590
quote_text=quote_text,
591+
refs=refs,
467592
attachments=attachments,
468593
source_platform=original_source_platform or None,
469594
source_user_name=original_source_user_name or None,
@@ -508,6 +633,7 @@ def handle_reply(
508633
priority=priority,
509634
reply_required=reply_required,
510635
event_id=event_id,
636+
refs=refs,
511637
attachments=attachments,
512638
)
513639
for actor in list_actors(group):

src/cccc/daemon/ops/maintenance_ops.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,9 @@ def handle_send_cross_group(
9898
attachments_raw = args.get("attachments")
9999
if attachments_raw:
100100
return _error("attachments_not_supported", "attachments are not supported for cross-group messages yet")
101+
refs_raw = args.get("refs")
102+
if isinstance(refs_raw, list) and any(isinstance(item, dict) for item in refs_raw):
103+
return _error("refs_not_supported", "quoted refs are not supported for cross-group messages yet")
101104
if priority not in ("normal", "attention"):
102105
return _error("invalid_priority", "priority must be 'normal' or 'attention'")
103106
if not src_group_id:

src/cccc/ports/mcp/handlers/cccc_messaging.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def message_send(
4343
to: Optional[List[str]] = None,
4444
priority: str = "normal",
4545
reply_required: bool = False,
46+
refs: Optional[List[Dict[str, Any]]] = None,
4647
) -> Dict[str, Any]:
4748
"""Send a message to the group (or cross-group)."""
4849
text = _normalize_runtime_escaped_text(group_id=group_id, actor_id=actor_id, text=text)
@@ -64,6 +65,7 @@ def message_send(
6465
"to": to if to is not None else [],
6566
"priority": prio,
6667
"reply_required": reply_required_flag,
68+
"refs": refs if refs is not None else [],
6769
},
6870
}
6971
)
@@ -79,6 +81,7 @@ def message_send(
7981
"path": "",
8082
"priority": prio,
8183
"reply_required": reply_required_flag,
84+
"refs": refs if refs is not None else [],
8285
},
8386
}
8487
)
@@ -93,6 +96,7 @@ def message_reply(
9396
to: Optional[List[str]] = None,
9497
priority: str = "normal",
9598
reply_required: bool = False,
99+
refs: Optional[List[Dict[str, Any]]] = None,
96100
) -> Dict[str, Any]:
97101
"""Reply to a message."""
98102
if not str(reply_to or "").strip():
@@ -113,6 +117,7 @@ def message_reply(
113117
"to": to if to is not None else [],
114118
"priority": prio,
115119
"reply_required": reply_required_flag,
120+
"refs": refs if refs is not None else [],
116121
},
117122
}
118123
)

src/cccc/ports/mcp/server.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,12 +281,14 @@ def _handle_cccc_namespace(name: str, arguments: Dict[str, Any]) -> Optional[Dic
281281
gid = _resolve_group_id(arguments)
282282
aid = _resolve_self_actor_id(arguments)
283283
to_raw = arguments.get("to")
284+
refs_raw = arguments.get("refs")
284285
if isinstance(to_raw, list):
285286
to_val: Optional[List[str]] = [str(x).strip() for x in to_raw if str(x).strip()]
286287
elif isinstance(to_raw, str) and to_raw.strip():
287288
to_val = [to_raw.strip()]
288289
else:
289290
to_val = None
291+
refs_val = [item for item in refs_raw if isinstance(item, dict)] if isinstance(refs_raw, list) else None
290292
return message_send(
291293
group_id=gid,
292294
dst_group_id=arguments.get("dst_group_id"),
@@ -295,18 +297,21 @@ def _handle_cccc_namespace(name: str, arguments: Dict[str, Any]) -> Optional[Dic
295297
to=to_val,
296298
priority=str(arguments.get("priority") or "normal"),
297299
reply_required=coerce_bool(arguments.get("reply_required"), default=False),
300+
refs=refs_val,
298301
)
299302

300303
if name == "cccc_message_reply":
301304
gid = _resolve_group_id(arguments)
302305
aid = _resolve_self_actor_id(arguments)
303306
to_raw = arguments.get("to")
307+
refs_raw = arguments.get("refs")
304308
if isinstance(to_raw, list):
305309
to_val_reply: Optional[List[str]] = [str(x).strip() for x in to_raw if str(x).strip()]
306310
elif isinstance(to_raw, str) and to_raw.strip():
307311
to_val_reply = [to_raw.strip()]
308312
else:
309313
to_val_reply = None
314+
refs_val_reply = [item for item in refs_raw if isinstance(item, dict)] if isinstance(refs_raw, list) else None
310315
reply_to = str(arguments.get("event_id") or arguments.get("reply_to") or "").strip()
311316
return message_reply(
312317
group_id=gid,
@@ -316,6 +321,7 @@ def _handle_cccc_namespace(name: str, arguments: Dict[str, Any]) -> Optional[Dic
316321
to=to_val_reply,
317322
priority=str(arguments.get("priority") or "normal"),
318323
reply_required=coerce_bool(arguments.get("reply_required"), default=False),
324+
refs=refs_val_reply,
319325
)
320326

321327
if name == "cccc_file":

src/cccc/ports/mcp/toolspecs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ def _obj(properties: dict, required: list[str] | None = None) -> dict:
118118
},
119119
"priority": {"type": "string", "enum": ["normal", "attention"], "default": "normal"},
120120
"reply_required": {"type": "boolean", "default": False},
121+
"refs": {"type": "array", "items": {"type": "object"}},
121122
},
122123
required=["text"],
123124
),
@@ -140,6 +141,7 @@ def _obj(properties: dict, required: list[str] | None = None) -> dict:
140141
},
141142
"priority": {"type": "string", "enum": ["normal", "attention"], "default": "normal"},
142143
"reply_required": {"type": "boolean", "default": False},
144+
"refs": {"type": "array", "items": {"type": "object"}},
143145
},
144146
required=["text"],
145147
),

0 commit comments

Comments
 (0)