cctally 1.35.0 → 1.36.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/CHANGELOG.md +5 -0
- package/bin/_lib_conversation.py +38 -0
- package/bin/_lib_conversation_query.py +10 -0
- package/dashboard/static/assets/{index-CXakskUV.js → index-DxQXelz7.js} +19 -17
- package/dashboard/static/assets/{index-Q7C0vruA.css → index-a_BOAOJ_.css} +1 -1
- package/dashboard/static/dashboard.html +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,11 @@ based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
5
5
|
|
|
6
6
|
## [Unreleased]
|
|
7
7
|
|
|
8
|
+
## [1.36.0] - 2026-06-11
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Conversation viewer: AskUserQuestion, TodoWrite, and ExitPlanMode now render as dedicated cards — the question-and-chosen-answer, a live checklist with progress, and the rendered plan with its approve/reject outcome — instead of raw JSON tool chips (#177 S2).
|
|
12
|
+
|
|
8
13
|
## [1.35.0] - 2026-06-11
|
|
9
14
|
|
|
10
15
|
### Added
|
package/bin/_lib_conversation.py
CHANGED
|
@@ -104,6 +104,7 @@ def _normalize(obj, t, offset):
|
|
|
104
104
|
elif any(b["kind"] == "tool_result" for b in blocks):
|
|
105
105
|
entry_type = TOOL_RESULT
|
|
106
106
|
_attach_subagent_result(blocks, obj) # #166: record-level toolUseResult
|
|
107
|
+
_attach_ask_answers(blocks, obj) # #177 S2: AskUserQuestion answers
|
|
107
108
|
# tool_result rows are stored but NOT indexed as prose (spec §2). A
|
|
108
109
|
# user line that mixes a text block with a tool_result block must not
|
|
109
110
|
# leak that text into the FTS index; the full content stays in
|
|
@@ -249,6 +250,43 @@ def _attach_subagent_result(blocks, obj):
|
|
|
249
250
|
block["subagent_meta"] = meta
|
|
250
251
|
|
|
251
252
|
|
|
253
|
+
def _attach_ask_answers(blocks, obj):
|
|
254
|
+
"""Stash an AskUserQuestion's structured answers (+ annotations) onto its
|
|
255
|
+
single tool_result block (#177 S2), so the chosen option(s) have a robust
|
|
256
|
+
source the client can highlight without parsing the harness result string.
|
|
257
|
+
Self-identifying: fires only when toolUseResult carries an ``answers`` dict
|
|
258
|
+
(a key distinctive to AskUserQuestion), so no cross-record tool-name lookup
|
|
259
|
+
is needed. Same exactly-one-result-block guard as _attach_subagent_result.
|
|
260
|
+
|
|
261
|
+
answers/annotations are BOUNDED through _bound_input before storage — a
|
|
262
|
+
free-form "Other" answer or a long annotation note is attacker-controlled
|
|
263
|
+
free text, and every other free-text payload in this parser is capped
|
|
264
|
+
before it reaches blocks_json. Reusing _bound_input applies the same
|
|
265
|
+
five-axis cap (leaf/key/node/depth/total). The bound's truncation flag is
|
|
266
|
+
intentionally NOT surfaced (no answer-level "truncated" affordance — a
|
|
267
|
+
>8000-char option answer is not a realistic shape; the cap is a backstop).
|
|
268
|
+
|
|
269
|
+
An EMPTY answers dict is treated as no-capture (symmetric with the empty-
|
|
270
|
+
annotations drop below): emitting ``ask_answers={}`` would set a falsy
|
|
271
|
+
``answers`` on the tool_call and suppress the client's result-text
|
|
272
|
+
fallback, so a degenerate empty payload must no-op here instead."""
|
|
273
|
+
tur = obj.get("toolUseResult")
|
|
274
|
+
if not isinstance(tur, dict):
|
|
275
|
+
return
|
|
276
|
+
answers = tur.get("answers")
|
|
277
|
+
if not isinstance(answers, dict) or not answers: # require a non-empty dict
|
|
278
|
+
return
|
|
279
|
+
results = [b for b in blocks if b.get("kind") == "tool_result"]
|
|
280
|
+
if len(results) != 1:
|
|
281
|
+
return
|
|
282
|
+
bounded_ans, _ = _bound_input(answers)
|
|
283
|
+
results[0]["ask_answers"] = bounded_ans
|
|
284
|
+
anno = tur.get("annotations")
|
|
285
|
+
if isinstance(anno, dict) and anno:
|
|
286
|
+
bounded_anno, _ = _bound_input(anno)
|
|
287
|
+
results[0]["ask_annotations"] = bounded_anno
|
|
288
|
+
|
|
289
|
+
|
|
252
290
|
def _stringify(c):
|
|
253
291
|
if isinstance(c, str):
|
|
254
292
|
return c
|
|
@@ -483,6 +483,7 @@ def get_conversation(conn, session_id, *, after=None, limit=500):
|
|
|
483
483
|
# spawn tool_use id <-> tool_result tool_use_id; agent_id == subagent_key.
|
|
484
484
|
spawn_kind = {} # tool_use id -> subagent_type
|
|
485
485
|
agent_link = {} # tool_use id -> (agent_id, raw_meta)
|
|
486
|
+
ask_link = {} # tool_use id -> (answers, annotations) (#177 S2)
|
|
486
487
|
for it in items:
|
|
487
488
|
for b in it["blocks"]:
|
|
488
489
|
k = b.get("kind")
|
|
@@ -495,6 +496,10 @@ def get_conversation(conn, session_id, *, after=None, limit=500):
|
|
|
495
496
|
meta = b.pop("subagent_meta", None)
|
|
496
497
|
if aid and b.get("tool_use_id") is not None:
|
|
497
498
|
agent_link[b["tool_use_id"]] = (aid, meta or {})
|
|
499
|
+
ans = b.pop("ask_answers", None) # #177 S2
|
|
500
|
+
anno = b.pop("ask_annotations", None)
|
|
501
|
+
if ans is not None and b.get("tool_use_id") is not None:
|
|
502
|
+
ask_link[b["tool_use_id"]] = (ans, anno)
|
|
498
503
|
subagent_meta = {}
|
|
499
504
|
for _tuid, _kind in spawn_kind.items():
|
|
500
505
|
_link = agent_link.get(_tuid)
|
|
@@ -553,6 +558,11 @@ def get_conversation(conn, session_id, *, after=None, limit=500):
|
|
|
553
558
|
b["kind"] = "tool_call"
|
|
554
559
|
b["tool_use_id"] = b.pop("id", None)
|
|
555
560
|
b.setdefault("result", None)
|
|
561
|
+
link = ask_link.get(b["tool_use_id"]) # #177 S2
|
|
562
|
+
if link is not None:
|
|
563
|
+
b["answers"] = link[0]
|
|
564
|
+
if link[1]:
|
|
565
|
+
b["annotations"] = link[1]
|
|
556
566
|
|
|
557
567
|
# ---- Phase 4: classify injected meta items (skill / command / context) ----
|
|
558
568
|
# `meta` rows (the parser's isMeta classification) AND — only while the 005
|