cc-transcript 10.3.0__tar.gz → 10.4.0__tar.gz
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.
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/PKG-INFO +1 -1
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/activity_probe.py +13 -10
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/pyproject.toml +1 -1
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/activity.rs +61 -13
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/mining.rs +3 -13
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/types.rs +13 -1
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/Cargo.lock +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/Cargo.toml +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/LICENSE +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/README.md +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/__init__.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/__main__.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/_parser_rs.pyi +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/activity.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/backend.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/builders.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/cli.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/command.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/context.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/corrections.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/corrections_cli.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/cost.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/decisions.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/discovery.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/disktruth.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/evidence.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/extract/__init__.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/extract/correct.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/facts.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/filterspec.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/ids.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/judge/__init__.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/judge/llm.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/judge/similar.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/judge/verdicts.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/mining/__init__.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/mining/candidates.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/mining/confidence.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/mining/engine.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/mining/filterspec.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/mining/formats.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/mining/signals.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/mining/sourcekind.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/mining/spec.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/mining/store.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/models.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/notifications.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/parser.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/py.typed +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/query.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/render.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/rust.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/sentiment/__init__.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/sentiment/buckets.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/sentiment/engine.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/sentiment/lexicon.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/sentiment/scorespec.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/store.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/cc_transcript/tools.py +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/Cargo.toml +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/build.rs +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/data/afinn-en-165.tsv +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/data/domain_overrides.tsv +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/command.rs +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/event.rs +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/filter.rs +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/lexicon.rs +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/lib.rs +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/model.rs +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/parse.rs +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/protocol.rs +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/python.rs +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/score.rs +0 -0
- {cc_transcript-10.3.0 → cc_transcript-10.4.0}/rust/src/value.rs +0 -0
|
@@ -10,8 +10,8 @@ stays identical to. The verdict spans undelivered task notifications (the
|
|
|
10
10
|
turn, and pending async launches (async Agent/Task, live Workflow) session-wide — a
|
|
11
11
|
launch counts as completed only once its notification was delivered or drained, never
|
|
12
12
|
when merely enqueued — and flags unanswered non-human-facing tool calls in the current
|
|
13
|
-
turn as mid-tool. Compact-summary user lines do not open turns,
|
|
14
|
-
|
|
13
|
+
turn as mid-tool. Compact-summary user lines do not open turns, matching captain-hook,
|
|
14
|
+
which consumes this probe since the 10.2.0 shared-classifier fix.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
from __future__ import annotations
|
|
@@ -30,6 +30,7 @@ from cc_transcript.models import (
|
|
|
30
30
|
)
|
|
31
31
|
from cc_transcript.notifications import Notifications
|
|
32
32
|
from cc_transcript.parser import parse_events_from_bytes
|
|
33
|
+
from cc_transcript.tools import expand_tool_names, matches_names
|
|
33
34
|
|
|
34
35
|
if TYPE_CHECKING:
|
|
35
36
|
from collections.abc import Sequence
|
|
@@ -39,7 +40,10 @@ if TYPE_CHECKING:
|
|
|
39
40
|
from cc_transcript.models import TranscriptEvent
|
|
40
41
|
|
|
41
42
|
DEFAULT_WAITING_TOOLS = frozenset({"Monitor", "ScheduleWakeup", "SendMessage", "TeamCreate"})
|
|
42
|
-
DEFAULT_HUMAN_FACING_TOOLS =
|
|
43
|
+
DEFAULT_HUMAN_FACING_TOOLS = expand_tool_names("AskUserQuestion|ExitPlanMode")
|
|
44
|
+
|
|
45
|
+
BACKGROUND_TOOLS = expand_tool_names("Agent|Task|Bash")
|
|
46
|
+
TASK_TOOLS = expand_tool_names("Agent|Task")
|
|
43
47
|
|
|
44
48
|
PendingKind = Literal[
|
|
45
49
|
"waiting_tool",
|
|
@@ -85,13 +89,12 @@ class SessionActivityProbe:
|
|
|
85
89
|
|
|
86
90
|
|
|
87
91
|
def ephemeral_wait(block: ToolUseBlock, waiting_tools: frozenset[str]) -> PendingKind | None:
|
|
88
|
-
if block.name
|
|
92
|
+
if matches_names(block.name, waiting_tools):
|
|
89
93
|
return "waiting_tool"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return "subagentless_task"
|
|
94
|
+
if matches_names(block.name, BACKGROUND_TOOLS) and block.input.get("run_in_background") is True:
|
|
95
|
+
return "background"
|
|
96
|
+
if matches_names(block.name, TASK_TOOLS) and not isinstance(block.input.get("subagent_type"), str):
|
|
97
|
+
return "subagentless_task"
|
|
95
98
|
return None
|
|
96
99
|
|
|
97
100
|
|
|
@@ -151,7 +154,7 @@ def probe_events(
|
|
|
151
154
|
waiting_kind = (ephemeral_wait(block, waiting_tools) if in_current_turn else None) or pending_async(
|
|
152
155
|
block, result, notifications
|
|
153
156
|
)
|
|
154
|
-
unmatched = in_current_turn and result is None and block.name
|
|
157
|
+
unmatched = in_current_turn and result is None and not matches_names(block.name, human_facing_tools)
|
|
155
158
|
is_waiting = is_waiting or waiting_kind is not None
|
|
156
159
|
mid_tool = mid_tool or unmatched
|
|
157
160
|
kind = waiting_kind or ("mid_tool" if unmatched else None)
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
use std::collections::{HashMap, HashSet};
|
|
2
2
|
|
|
3
|
-
use
|
|
3
|
+
use once_cell::sync::Lazy;
|
|
4
|
+
|
|
5
|
+
use crate::types::{matches_names, Entry, ToolResultBlock, ToolUseBlock, UserEntry};
|
|
4
6
|
use crate::value::{field, field_str};
|
|
5
7
|
|
|
6
8
|
const NOTIFICATION_MARKER: &str = "<task-notification>";
|
|
7
9
|
|
|
10
|
+
// tools.py expand_tool_names("Agent|Task|Bash") / ("Agent|Task"), pre-expanded
|
|
11
|
+
// here because the alias table never crosses the language boundary.
|
|
12
|
+
static BACKGROUND_TOOLS: Lazy<HashSet<String>> =
|
|
13
|
+
Lazy::new(|| HashSet::from(["Agent", "Task", "Bash", "Execute"].map(String::from)));
|
|
14
|
+
static TASK_TOOLS: Lazy<HashSet<String>> =
|
|
15
|
+
Lazy::new(|| HashSet::from(["Agent", "Task"].map(String::from)));
|
|
16
|
+
|
|
8
17
|
pub struct ActivityOpts {
|
|
9
18
|
pub waiting_tools: HashSet<String>,
|
|
10
19
|
pub human_facing_tools: HashSet<String>,
|
|
@@ -16,7 +25,11 @@ impl Default for ActivityOpts {
|
|
|
16
25
|
waiting_tools: HashSet::from(
|
|
17
26
|
["Monitor", "ScheduleWakeup", "SendMessage", "TeamCreate"].map(String::from),
|
|
18
27
|
),
|
|
19
|
-
|
|
28
|
+
// tools.py expand_tool_names("AskUserQuestion|ExitPlanMode"), pre-expanded
|
|
29
|
+
// here because the alias table never crosses the language boundary.
|
|
30
|
+
human_facing_tools: HashSet::from(
|
|
31
|
+
["AskUserQuestion", "ExitPlanMode", "ExitSpecMode"].map(String::from),
|
|
32
|
+
),
|
|
20
33
|
}
|
|
21
34
|
}
|
|
22
35
|
}
|
|
@@ -61,8 +74,8 @@ pub struct SessionActivity {
|
|
|
61
74
|
|
|
62
75
|
/// Whether a user entry opens a turn (activity.py ``native_user_classifier``):
|
|
63
76
|
/// a real prompt — non-meta, non-sidechain, not an interruption marker, with
|
|
64
|
-
/// non-blank text. Compact-summary exclusion
|
|
65
|
-
///
|
|
77
|
+
/// non-blank text. Compact-summary exclusion matches captain-hook, which
|
|
78
|
+
/// consumes this probe since the 10.2.0 shared-classifier fix.
|
|
66
79
|
fn opens_turn(user: &UserEntry) -> bool {
|
|
67
80
|
!(user.meta.is_meta || user.meta.is_sidechain || user.meta.is_compact_summary || user.interrupted())
|
|
68
81
|
&& !user.content.text().trim().is_empty()
|
|
@@ -78,18 +91,19 @@ fn current_turn_start(entries: &[Entry]) -> usize {
|
|
|
78
91
|
}
|
|
79
92
|
|
|
80
93
|
/// conditions.py ``ephemeral_wait``: a waiting tool, a backgrounded
|
|
81
|
-
/// Agent/Task/Bash, or a subagentless Agent/Task
|
|
94
|
+
/// Agent/Task/Bash, or a subagentless Agent/Task — every name matched alias-
|
|
95
|
+
/// and MCP-prefix-aware (tools.py matches_names).
|
|
82
96
|
fn ephemeral_wait(tool_use: &ToolUseBlock, waiting_tools: &HashSet<String>) -> Option<PendingKind> {
|
|
83
|
-
if
|
|
97
|
+
if matches_names(&tool_use.name, waiting_tools) {
|
|
84
98
|
return Some(PendingKind::WaitingTool);
|
|
85
99
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
_ => None,
|
|
100
|
+
if matches_names(&tool_use.name, &BACKGROUND_TOOLS) && tool_use.run_in_background == Some(true) {
|
|
101
|
+
return Some(PendingKind::Background);
|
|
102
|
+
}
|
|
103
|
+
if matches_names(&tool_use.name, &TASK_TOOLS) && tool_use.subagent_type.is_none() {
|
|
104
|
+
return Some(PendingKind::SubagentlessTask);
|
|
92
105
|
}
|
|
106
|
+
None
|
|
93
107
|
}
|
|
94
108
|
|
|
95
109
|
/// conditions.py ``pending_async``: an async Agent/Task launch or a live
|
|
@@ -217,7 +231,7 @@ pub fn session_activity(entries: &[Entry], opts: &ActivityOpts) -> SessionActivi
|
|
|
217
231
|
.or_else(|| pending_async(tool_use, result, ¬ifications));
|
|
218
232
|
let unmatched = in_current_turn
|
|
219
233
|
&& result.is_none()
|
|
220
|
-
&& !
|
|
234
|
+
&& !matches_names(&tool_use.name, &opts.human_facing_tools);
|
|
221
235
|
is_waiting |= waiting_kind.is_some();
|
|
222
236
|
mid_tool |= unmatched;
|
|
223
237
|
let Some(kind) = waiting_kind.or(unmatched.then_some(PendingKind::MidTool)) else {
|
|
@@ -545,6 +559,17 @@ mod tests {
|
|
|
545
559
|
assert!(activity.pending.is_empty());
|
|
546
560
|
}
|
|
547
561
|
|
|
562
|
+
#[test]
|
|
563
|
+
fn background_execute_alias_in_current_turn_is_waiting() {
|
|
564
|
+
let entries = vec![
|
|
565
|
+
user("build it"),
|
|
566
|
+
tool_use("Execute", "e1", r#"{"command":"make","run_in_background":true}"#),
|
|
567
|
+
];
|
|
568
|
+
let activity = activity(&entries);
|
|
569
|
+
assert!(activity.is_waiting);
|
|
570
|
+
assert_eq!(activity.pending[0].kind, PendingKind::Background);
|
|
571
|
+
}
|
|
572
|
+
|
|
548
573
|
#[test]
|
|
549
574
|
fn subagentless_agent_is_waiting() {
|
|
550
575
|
let entries = vec![
|
|
@@ -601,6 +626,17 @@ mod tests {
|
|
|
601
626
|
assert_eq!(activity.pending[0].kind, PendingKind::WaitingTool);
|
|
602
627
|
}
|
|
603
628
|
|
|
629
|
+
#[test]
|
|
630
|
+
fn mcp_prefixed_waiting_tool_is_waiting() {
|
|
631
|
+
let entries = vec![
|
|
632
|
+
user("ping the pool"),
|
|
633
|
+
tool_use("mcp__pool__SendMessage", "s1", r#"{"text":"hi"}"#),
|
|
634
|
+
];
|
|
635
|
+
let activity = activity(&entries);
|
|
636
|
+
assert!(activity.is_waiting);
|
|
637
|
+
assert_eq!(activity.pending[0].kind, PendingKind::WaitingTool);
|
|
638
|
+
}
|
|
639
|
+
|
|
604
640
|
#[test]
|
|
605
641
|
fn pending_ask_user_question_is_quiet() {
|
|
606
642
|
let entries = vec![
|
|
@@ -613,6 +649,18 @@ mod tests {
|
|
|
613
649
|
assert!(activity.pending.is_empty());
|
|
614
650
|
}
|
|
615
651
|
|
|
652
|
+
#[test]
|
|
653
|
+
fn mcp_prefixed_human_facing_tool_is_not_mid_tool() {
|
|
654
|
+
let entries = vec![
|
|
655
|
+
user("choose"),
|
|
656
|
+
tool_use("mcp__someserver__AskUserQuestion", "q1", r#"{"questions":[]}"#),
|
|
657
|
+
];
|
|
658
|
+
let activity = activity(&entries);
|
|
659
|
+
assert!(!activity.is_waiting);
|
|
660
|
+
assert!(!activity.mid_tool);
|
|
661
|
+
assert!(activity.pending.is_empty());
|
|
662
|
+
}
|
|
663
|
+
|
|
616
664
|
#[test]
|
|
617
665
|
fn unmatched_bash_in_current_turn_is_mid_tool() {
|
|
618
666
|
let entries = vec![user("list"), tool_use("Bash", "b1", r#"{"command":"ls"}"#)];
|
|
@@ -13,7 +13,9 @@ use crate::protocol::{
|
|
|
13
13
|
embedded_user_text, interrupt_marker, is_bare_interrupt_marker, ANSWERED_PREFIX, ANSWERED_TRAILER,
|
|
14
14
|
DENIAL_PREFIX, INTERRUPT_MARKER_RE,
|
|
15
15
|
};
|
|
16
|
-
use crate::types::{
|
|
16
|
+
use crate::types::{
|
|
17
|
+
matches_names, tool_use_index, Entry, EntryMeta, Question, ToolResultBlock, ToolUseBlock, UserEntry,
|
|
18
|
+
};
|
|
17
19
|
use crate::value::{field, field_bool, field_str};
|
|
18
20
|
|
|
19
21
|
// Source kinds (mining/sourcekind.py).
|
|
@@ -478,18 +480,6 @@ impl Events {
|
|
|
478
480
|
}
|
|
479
481
|
}
|
|
480
482
|
|
|
481
|
-
/// matches_names (tools.py matches_names): exact membership, or the ``<tool>``
|
|
482
|
-
/// segment of an ``mcp__<server>__<tool>`` name split on the first two ``__``.
|
|
483
|
-
/// Alias closure happens Python-side (tools.py expand_tool_names); the spec JSON
|
|
484
|
-
/// arrives pre-expanded and the alias table never crosses the language boundary.
|
|
485
|
-
fn matches_names(actual: &str, names: &HashSet<String>) -> bool {
|
|
486
|
-
names.contains(actual)
|
|
487
|
-
|| actual
|
|
488
|
-
.strip_prefix("mcp__")
|
|
489
|
-
.and_then(|rest| rest.split_once("__"))
|
|
490
|
-
.is_some_and(|(_, tool)| names.contains(tool))
|
|
491
|
-
}
|
|
492
|
-
|
|
493
483
|
// ── text-shape helpers (mining/signals.py) ───────────────────────────────────
|
|
494
484
|
|
|
495
485
|
/// marker_in (mining/signals.py marker_in): whether any tool-result block's
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
use std::collections::HashMap;
|
|
1
|
+
use std::collections::{HashMap, HashSet};
|
|
2
2
|
|
|
3
3
|
use chrono::{DateTime, FixedOffset};
|
|
4
4
|
use sonic_rs::Value;
|
|
@@ -225,6 +225,18 @@ pub fn tool_use_index(entries: &[Entry]) -> HashMap<&str, &ToolUseBlock> {
|
|
|
225
225
|
.collect()
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
/// matches_names (tools.py matches_names): exact membership, or the ``<tool>``
|
|
229
|
+
/// segment of an ``mcp__<server>__<tool>`` name split on the first two ``__``.
|
|
230
|
+
/// Alias closure happens Python-side (tools.py expand_tool_names); spec JSON
|
|
231
|
+
/// arrives pre-expanded and the alias table never crosses the language boundary.
|
|
232
|
+
pub(crate) fn matches_names(actual: &str, names: &HashSet<String>) -> bool {
|
|
233
|
+
names.contains(actual)
|
|
234
|
+
|| actual
|
|
235
|
+
.strip_prefix("mcp__")
|
|
236
|
+
.and_then(|rest| rest.split_once("__"))
|
|
237
|
+
.is_some_and(|(_, tool)| names.contains(tool))
|
|
238
|
+
}
|
|
239
|
+
|
|
228
240
|
/// The space-joined text of the ``Text`` blocks — the ``text`` field of the
|
|
229
241
|
/// Python ``AssistantEvent``/``PrintMessage``.
|
|
230
242
|
pub fn joined_text(blocks: &[ContentBlock]) -> String {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|