[2026.04 Week 2] Five Trending Repos of the Week
TL;DR
Agent skills still dominate, but saturation is setting in. Nearly half of the 100 repos trending on GitHub this week were Claude Code skills, agent harnesses, or skill registries.
On-device inference is gaining momentum. Multiple projects pushed LLMs and ML models onto phones and laptops without cloud calls.
Voice AI continued to trend. Tokenizer-free TTS and open-source voice cloning projects appeared, signaling sustained interest in speech generation.
People started cloning themselves into skill files. Five projects turned a person’s thinking style or domain expertise into a reusable agent skill.
This week’s picks:
goose (⭐ 41.5k). Open-source AI agent with a desktop app, CLI, and API. Connects to 70+ tools through MCP and runs them concurrently.
VoxCPM (⭐ 11.1k). Tokenizer-free TTS that generates speech directly from continuous representations instead of discrete audio tokens. Supports 30 languages, voice design from text descriptions, and voice cloning.
apfel (⭐ 4.3k). Gives you terminal and server access to the LLM already on every Apple Silicon Mac, without API keys or cloud calls. The model is already on your machine.
gallery (⭐ 20.6k). Google’s app for running open-source LLMs on your phone, fully offline and private. Your data never leaves the device.
pretext (⭐ 43.2k). Pure JS library for multiline text measurement and layout that avoids DOM reflows entirely. Relayouts take ~0.0002ms per text block.
aaif-goose/goose
⭐ 41.5k · Rust
Goose is an open-source AI agent that wires up MCP servers as tool extensions.
When an LLM returns five tool calls at once, goose fires them all concurrently. Each tool call produces a ToolStream that wraps two things: the async future doing the actual work, and the MCP notification channel. A combiner function races both with tokio::select!, yielding whichever completes first.
crates/goose/src/agents/agent.rs:171-204
pub enum ToolStreamItem<T> {
Message(ServerNotification),
Result(T),
}
pub fn tool_stream<S, F>(rx: S, done: F) -> ToolStream
where
S: Stream<Item = ServerNotification> + Send + Unpin + 'static,
F: Future<Output = ToolResult<Vec<Content>>> + Send + 'static,
{
Box::pin(async_stream::stream! {
tokio::pin!(done);
let mut rx = rx;
loop {
tokio::select! {
Some(msg) = rx.next() => {
yield ToolStreamItem::Message(msg);
}
r = &mut done => {
yield ToolStreamItem::Result(r);
break;
}
}
}
})
}Each tool produces a ToolStream. The agent tags every stream item with a request_id, then feeds them into stream::select_all(), which polls every stream concurrently and yields items in completion order.
crates/goose/src/agents/agent.rs:1435-1496
let with_id = tool_futures
.into_iter()
.map(|(request_id, stream)| {
stream.map(move |item| (request_id.clone(), item))
})
.collect::<Vec<_>>();
let mut combined = stream::select_all(with_id);
// ... inside tokio::select! { biased; ... }
tool_item = combined.next() => {
match tool_item {
Some((request_id, item)) => {
match item {
ToolStreamItem::Result(output) => {
// ...
response.add_tool_response_with_metadata(
request_id, output, metadata,
);
}
ToolStreamItem::Message(msg) => {
yield AgentEvent::McpNotification((request_id, msg));
}
}
}
None => break,
}
}The request_id ties it all together. Tool results go into a request_to_response_map keyed by ID, so each result lands in the correct response message. Notifications yield immediately as AgentEvent::McpNotification with the ID attached, so the UI can attribute progress updates to the right tool.
The pattern means goose can run 100 concurrent tool calls without losing track of which response belongs to which call. Notifications stream in real time while results accumulate, and the LLM never sees the parallelism. It gets a complete set of tool responses.
OpenBMB/VoxCPM
⭐ 11.1k · Python
VoxCPM2 is a text-to-speech system that generates multilingual speech and clones voices.
Most TTS models work like text LLMs do. They convert audio into discrete tokens from a fixed vocabulary (a “codebook”), then train a language model to predict the next audio token. The codebook is typically learned by a VQ-VAE (Vector Quantized Variational Autoencoder), which compresses raw audio into a small set of entries. The problem is that each entry is a lossy approximation. When the model generates hundreds of tokens in sequence, these small rounding errors compound, degrading audio quality over longer utterances.
VoxCPM2 takes a different approach. Instead of converting audio to discrete tokens, it keeps audio as continuous floating-point vectors throughout the entire pipeline.
The key component is a ScalarQuantizationLayer that adds just enough structure for the model to learn, without forcing audio into a fixed codebook.
src/voxcpm/modules/layers/scalar_quantization_layer.py:1-27
class ScalarQuantizationLayer(nn.Module):
def __init__(self, in_dim, out_dim, latent_dim: int = 64, scale: int = 9):
super().__init__()
self.in_proj = nn.Linear(in_dim, latent_dim)
self.out_proj = nn.Linear(latent_dim, out_dim)
self.scale = scale
def forward(self, hidden):
hidden = self.in_proj(hidden)
hidden = torch.tanh(hidden)
if self.training:
quantized = torch.round(hidden * self.scale) / self.scale
hidden = hidden + (quantized - hidden).detach()
else:
hidden = torch.round(hidden * self.scale) / self.scale
return self.out_proj(hidden)Here is what this layer does step by step. First, in_proj projects the input down to 64 dimensions. Then tanh squashes each value into the [-1, 1] range. Finally, round(hidden * 9) / 9 snaps each value to the nearest point on a grid of 19 evenly spaced values (-1.0, -0.889, -0.778, ..., 0.889, 1.0). This gives the model structure to latch onto during training without forcing it to pick from a fixed set of codebook entries.
There is a training trick here. Rounding is not differentiable, so gradients cannot flow through it during backpropagation. The .detach() call solves this. hidden + (quantized - hidden).detach() makes the forward pass use the quantized value, but the backward pass sees only hidden, as if the rounding never happened. This is called a “straight-through estimator.” The model learns as if the values are continuous, but operates on quantized values.
The language model processes text and audio together. Text tokens go through the standard embedding table. Audio goes through a separate encoder that produces continuous feature vectors. Both get combined into a single sequence of embeddings.
src/voxcpm/model/voxcpm2.py:268-279
feat_embed = self.feat_encoder(audio_feats)
feat_embed = self.enc_to_lm_proj(feat_embed)
scale_emb = getattr(self.config.lm_config, "scale_emb", 1.0)
if not getattr(self.config.lm_config, "use_mup", False):
scale_emb = 1.0
text_embed = self.base_lm.embed_tokens(text_tokens) * scale_emb
combined_embed = text_mask.unsqueeze(-1) * text_embed + audio_mask.unsqueeze(-1) * feat_embed
enc_outputs, _ = self.base_lm(inputs_embeds=combined_embed, is_causal=True)
enc_outputs = enc_outputs.to(self._dtype())
enc_outputs = self.fsq_layer(enc_outputs) * audio_mask.unsqueeze(-1) + enc_outputs * text_mask.unsqueeze(-1)The masks are binary tensors that select which positions are text and which are audio. text_mask * text_embed + audio_mask * feat_embed interleaves them into one sequence. The language model runs a single forward pass over this combined input. After the LM runs, fsq_layer (the scalar quantization layer from above) processes only the audio positions via * audio_mask, while text positions keep their original LM output via * text_mask. Text and audio share the same transformer, but quantization only touches audio.
The full pipeline stays continuous end to end. An audio encoder compresses raw audio into continuous vectors. The language model processes those vectors alongside text embeddings. Then a diffusion-based decoder (similar to image diffusion models, but for audio waveforms) generates new audio from the model’s continuous output. During inference, each generated audio segment feeds back into the language model as continuous input for the next step. No codebook means no accumulated rounding errors across the sequence.
Arthur-Ficial/apfel
⭐ 4.3k · Swift
Apple’s FoundationModels framework runs an LLM on-device without API keys or cloud calls. The catch is a hard 4096-token context window, input and output combined. apfel wraps this framework as a UNIX tool and OpenAI-compatible server.
The main challenge is fitting conversations into that tiny window. apfel implements five trimming strategies, selected per session. The dispatcher checks whether even the system prompt plus the current message fits, and bails early if not.
func trimHistoryEntriesToBudget(
baseEntries: [Transcript.Entry],
historyEntries: [Transcript.Entry],
finalEntry: Transcript.Entry? = nil,
budget: Int,
config: ContextConfig = .defaults
) async -> [Transcript.Entry]? {
let requiredEntries = assembleTranscriptEntries(base: baseEntries, history: [], final: finalEntry)
guard await fitsTranscriptBudget(requiredEntries, budget: budget) else {
return nil
}
switch config.strategy {
case .newestFirst:
return await trimNewestFirst(...)
case .oldestFirst:
return await trimOldestFirst(...)
case .slidingWindow:
return await trimSlidingWindow(...)
case .summarize:
return await trimWithSummary(...)
case .strict:
let all = assembleTranscriptEntries(base: baseEntries, history: historyEntries, final: finalEntry)
return await fitsTranscriptBudget(all, budget: budget) ? all : nil
}
}The default strategy, newestFirst, uses binary search to find exactly how many recent conversation turns fit within the token budget. It calls the real SystemLanguageModel.tokenCount(for:) API at each probe instead of estimating.
func maxNewestHistoryCountThatFits(
base: [Transcript.Entry],
history: [Transcript.Entry],
final: Transcript.Entry?,
budget: Int
) async -> Int {
guard !history.isEmpty else { return 0 }
var low = 0
var high = history.count
while low < high {
let mid = (low + high + 1) / 2
let candidate = history.suffix(mid)
if await fitsTranscriptBudget(base: base, history: candidate, final: final, budget: budget) {
low = mid
} else {
high = mid - 1
}
}
return low
}The binary search is O(log N) probes, each calling the real token counter. history.suffix(mid) takes the most recent mid entries, so the search finds the maximum number of recent turns that fit alongside the system prompt and new input.
The summarize strategy splits the budget 50/50, keeps recent turns verbatim in one half, and uses the on-device model to compress older turns into a 2-3 sentence summary for the other half. If summarization fails, it falls back to newestFirst.
Of the 4096-token window, 512 are reserved for output by default, leaving 3584 for the system prompt, history, and new input. After every assistant response, apfel checks whether the transcript exceeds the budget and rotates context if needed, re-injecting MCP tool definitions so tools don’t silently stop working.
google-ai-edge/gallery
⭐ 20.6k · Kotlin
Gallery is Google’s reference app for running ML models on Android devices. It supports Gemma models through two runtimes, LiteRT (pure offline) and AI Core (cloud-integrated). LiteRT surfaces the model’s reasoning through a separate channel from the response.
The contract between the runtime and the UI is a three-parameter callback.
Android/src/app/src/main/java/com/google/ai/edge/gallery/runtime/LlmModelHelper.kt:26-27
typealias ResultListener =
(partialResult: String, done: Boolean, partialThinkingResult: String?) -> UnitThe third parameter, partialThinkingResult, carries thinking traces. During LiteRT inference, the model’s Message object has a channels map. The "thought" key holds the step-by-step reasoning the model produces alongside its response.
Android/src/app/src/main/java/com/google/ai/edge/gallery/ui/llmchat/LlmChatModelHelper.kt:279-301
conversation.sendMessageAsync(
Contents.of(contents),
object : MessageCallback {
override fun onMessage(message: Message) {
resultListener(message.toString(), false, message.channels["thought"])
}
override fun onDone() {
resultListener("", true, null)
}
override fun onError(throwable: Throwable) {
// ...
}
},
extraContext ?: emptyMap(),
)message.channels["thought"] is the extraction point. Each streaming callback fires with both the regular text (via message.toString()) and whatever the model put in its thinking channel. While thinking is active, the ViewModel creates a separate ChatMessageThinking message in the UI. When partialThinkingResult becomes null, the ViewModel marks the thinking message as complete and starts accumulating the response in a new text message.
@Composable
fun MessageBodyThinking(thinkingText: String, inProgress: Boolean) {
var isExpanded by remember { mutableStateOf(false) }
if (inProgress) {
isExpanded = true
}
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp)) {
Row(modifier = Modifier.clickable { isExpanded = !isExpanded }
// ...
) {
Text(text = stringResource(R.string.show_thinking), /* ... */)
Icon(imageVector = if (isExpanded) Icons.Filled.ArrowDropUp else Icons.Filled.ArrowDropDown,
// ...
)
}
AnimatedVisibility(visible = isExpanded, enter = expandVertically(), exit = shrinkVertically()) {
// ... renders thinking text with a left-side vertical line
MarkdownText(text = thinkingText, smallFontSize = true, /* ... */)
}
}
}The UI auto-expands while thinking is in progress and becomes collapsible once the model finishes. The thinking panel renders with a left-side vertical line and smaller font to separate it from the response.
The AI Core runtime (for newer Gemma 4 models) doesn’t support this and always passes null for the thinking parameter. The thinking channel is LiteRT-only, tied to the on-device inference path.
chenglou/pretext
⭐ 43.2k · TypeScript
pretext is a pure JavaScript library for multiline text measurement and layout that avoids DOM reflows entirely.
Every time a browser component calls getBoundingClientRect() to measure text, it forces a synchronous layout reflow. When multiple components measure independently, the browser repeatedly reflows the entire document. For 500 text blocks, this can cost 30ms+ per frame. pretext eliminates that by splitting text measurement into two phases.
Phase one is prepare(). It runs once per text block, segments text via Intl.Segmenter, measures each segment through canvas measureText(), and caches the widths.
type PreparedCore = {
widths: number[] // Segment widths, e.g. [42.5, 4.4, 37.2]
lineEndFitAdvances: number[] // Width contribution when a line ends after this segment
lineEndPaintAdvances: number[] // Painted width for trailing segments (spaces = 0)
kinds: SegmentBreakKind[] // Break behavior, e.g. ['text', 'space', 'text']
simpleLineWalkFastPath: boolean // Normal text skips the complex line walker
breakableFitAdvances: (number[] | null)[] // Per-grapheme widths for overflow-wrap
discretionaryHyphenWidth: number
tabStopAdvance: number
chunks: PreparedLineChunk[] // Precompiled hard-break chunks
}Every measurement lands in parallel arrays. The segment cache is a two-level map keyed by (font, segment text), shared across all text blocks.
export function getSegmentMetrics(seg: string, cache: Map<string, SegmentMetrics>): SegmentMetrics {
let metrics = cache.get(seg)
if (metrics === undefined) {
const ctx = getMeasureContext()
metrics = {
width: ctx.measureText(seg).width,
containsCJK: isCJK(seg),
}
cache.set(seg, metrics)
}
return metrics
}Phase two is layout(). It runs on every window resize using only cached widths and integer arithmetic, with no canvas, DOM, or string operations.
// Layout prepared text at a given max width and caller-provided lineHeight.
// Pure arithmetic on cached widths — no canvas calls, no DOM reads, no string
// operations, no allocations.
// ~0.0002ms per text block. Call on every resize.
export function layout(prepared: PreparedText, maxWidth: number, lineHeight: number): LayoutResult {
const lineCount = countPreparedLines(getInternalPrepared(prepared), maxWidth)
return { lineCount, height: lineCount * lineHeight }
}countPreparedLines walks the cached widths and kinds arrays with a line-breaking state machine. For each segment, it adds the cached width to the current line, checks if it overflows maxWidth, and either continues or breaks. Trailing whitespace “hangs” past the line edge without triggering breaks, matching CSS behavior. Words wider than maxWidth break at grapheme boundaries using the pre-measured breakableFitAdvances.
500 text blocks reflow in about 0.1ms total, compared to 30ms+ with DOM-based measurement. The PreparedText handle is opaque and width-independent, so the same prepared text can be laid out at any container width without re-measuring.

