feat(plugins): add live timeline rows and client slash commands

Plugins can transform live timeline source items at render time, append durable plugin rows from daemon handlers, and register client-only slash commands in the composer.

Related to #3940 and #3952.
This commit is contained in:
Mohamed Boudra
2026-09-02 13:12:09 +02:00
committed by GitHub
parent 49784fac15
commit e34aea206e
66 changed files with 2122 additions and 511 deletions
@@ -0,0 +1,4 @@
# Inline thinking timeline example
This plugin replaces reasoning rows from the first streaming delta and renders the paced text
inline. Install the directory as `inline-thinking`, then run an agent that streams reasoning.
+26
View File
@@ -0,0 +1,26 @@
import type { PluginContext } from "@getpaseo/plugin";
import { InlineThinking, inlineThinkingSchema } from "./thinking.client";
export default function contribute(plugin: PluginContext) {
plugin.addTimelineTransformer({
id: "inline-thinking",
query: { itemType: "reasoning" },
transform: ({ item, phase }) => ({
items: [
{
type: "plugin",
kind: "inline-thinking",
version: 1,
data: { text: item.text, phase },
},
],
}),
});
plugin.addTimelineRenderer({
kind: "inline-thinking",
version: 1,
schema: inlineThinkingSchema,
Component: InlineThinking,
});
return () => {};
}
@@ -0,0 +1,3 @@
{
"id": "inline-thinking"
}
@@ -0,0 +1,21 @@
import type { PluginTimelineItemProps } from "@getpaseo/plugin";
import { useRevealedText } from "@getpaseo/plugin/react-native";
import { useMemo } from "react";
import { Text } from "react-native";
import { z } from "zod";
export const inlineThinkingSchema = z.object({
text: z.string(),
phase: z.enum(["streaming", "complete"]),
});
type InlineThinkingData = z.output<typeof inlineThinkingSchema>;
export function InlineThinking({ item, theme }: PluginTimelineItemProps<InlineThinkingData>) {
const text = useRevealedText(item.data.text, item.data.phase);
const style = useMemo(
() => ({ color: theme.colors.foregroundMuted }),
[theme.colors.foregroundMuted],
);
return <Text style={style}>{text}</Text>;
}
+1 -1
View File
@@ -8,7 +8,7 @@ import {
useRpc,
useWorkspace,
} from "@getpaseo/plugin";
import React, { useCallback, useMemo } from "react";
import { useCallback, useMemo } from "react";
import { Pressable, Text, View } from "react-native";
import { incrementRpc } from "./increment.shared";
+6 -7
View File
@@ -1,15 +1,14 @@
# Pi tasks timeline plugin example
This example implements the behavior proposed in Paseo PR #3751 as a plugin. It recognizes
completed Pi `todo` tool calls from both `@juicesharp/rpiv-todo` and Pi's
This example recognizes Pi `todo` tool calls from both `@juicesharp/rpiv-todo` and Pi's
`examples/extensions/todo.ts`, replaces the tool-call entry with a versioned `pi-task-list` item,
and renders the current task snapshot with a native React Native component.
and renders the current task snapshot with a native React Native component. It transforms both
running and completed calls.
The rpiv shape preserves `pending`, `in_progress`, and `completed`; deleted tombstones are omitted.
Pi's example shape maps `done` to `completed` or `pending`. Malformed results and unrelated tool
calls return `undefined`, leaving Paseo's original timeline entry unchanged.
The transformer is a pure client contribution. It receives the daemon's projected timeline item,
returns plain plugin item objects, and runs against projected history. Matching live events refresh
the authoritative projected tail first. The renderer validates `data` before Paseo mounts the
component.
The transformer is a pure client contribution. It receives each source item and its streaming phase
while Paseo builds the render model, then returns plain plugin item objects. The renderer validates
`data` before Paseo mounts the component.
@@ -15,6 +15,7 @@ function completedTodo(output: unknown) {
describe("Pi task timeline example", () => {
it("maps @juicesharp/rpiv-todo and drops deleted tasks", () => {
const result = transformPiTodoToolCall({
phase: "complete",
item: completedTodo({
content: [{ type: "text", text: "Created #2" }],
details: {
@@ -48,6 +49,7 @@ describe("Pi task timeline example", () => {
it("maps Pi's example todo extension", () => {
const result = transformPiTodoToolCall({
phase: "complete",
item: completedTodo({
details: {
todos: [
@@ -69,11 +71,29 @@ describe("Pi task timeline example", () => {
it("keeps unrelated and malformed tool calls unchanged", () => {
expect(
transformPiTodoToolCall({
phase: "complete",
item: { ...completedTodo({ details: { todos: [] } }), name: "write" },
}),
).toBeUndefined();
expect(
transformPiTodoToolCall({ item: completedTodo({ details: { phases: [] } }) }),
transformPiTodoToolCall({
phase: "complete",
item: completedTodo({ details: { phases: [] } }),
}),
).toBeUndefined();
});
it("maps a running todo tool call", () => {
const result = transformPiTodoToolCall({
phase: "streaming",
item: {
...completedTodo({ details: { todos: [{ text: "live task", done: false }] } }),
status: "running",
},
});
expect(result?.items[0]?.data).toEqual({
tasks: [{ text: "live task", status: "pending" }],
});
});
});
+1 -1
View File
@@ -53,7 +53,7 @@ function replacement(tasks: PiTask[]) {
}
export const transformPiTodoToolCall: ToolCallTransformer = ({ item }) => {
if (item.name !== "todo" || item.status !== "completed" || item.detail.type !== "unknown") {
if (item.name !== "todo" || item.detail.type !== "unknown") {
return;
}