@camerontaylor/paseo-plugin 0.12.0-fork.1 → 0.13.0-fork.2

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.
@@ -1,10 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PluginTranslationProvider, usePluginTranslationRuntime } from "./translations.js";
2
3
  import { useCallback } from "react";
3
4
  import { PluginClientStateProvider, usePluginClientStateSource } from "./client-state.js";
4
5
  import { PaseoApiProvider, usePaseoContextValue } from "./paseo-context.js";
5
6
  import { PluginRpcProvider, usePluginRpcContextValue } from "./rpc-context.js";
6
7
  /** Rebuilds plugin runtime contexts inside React Native portal hosts. */
7
8
  export function usePluginRuntimeContextBridge() {
9
+ const translations = usePluginTranslationRuntime();
8
10
  const paseo = usePaseoContextValue();
9
11
  const rpc = usePluginRpcContextValue();
10
12
  const state = usePluginClientStateSource();
@@ -13,7 +15,7 @@ export function usePluginRuntimeContextBridge() {
13
15
  }
14
16
  return useCallback((children) => {
15
17
  const content = state ? (_jsx(PluginClientStateProvider, { source: state, children: children })) : (children);
16
- return (_jsx(PaseoApiProvider, { paseo: paseo, children: _jsx(PluginRpcProvider, { invoke: rpc.invoke, children: content }) }));
17
- }, [paseo, rpc, state]);
18
+ return (_jsx(PaseoApiProvider, { paseo: paseo, children: _jsx(PluginRpcProvider, { invoke: rpc.invoke, children: _jsx(PluginTranslationProvider, { runtime: translations, children: content }) }) }));
19
+ }, [paseo, rpc, state, translations]);
18
20
  }
19
21
  //# sourceMappingURL=runtime-context-bridge.js.map
@@ -0,0 +1,6 @@
1
+ import type { JsonValue } from "@camerontaylor/paseo-protocol/agent-types";
2
+ import type { ZodType } from "zod";
3
+ import type { PluginTab } from "./contracts.js";
4
+ /** Validates the panel's persisted state, using the default for missing or invalid values. */
5
+ export declare function useTabState<T extends JsonValue>(tab: PluginTab, schema: ZodType<T>, defaultState: T): [T, (next: T) => void];
6
+ //# sourceMappingURL=tab.d.ts.map
@@ -0,0 +1,9 @@
1
+ import { useCallback, useMemo } from "react";
2
+ /** Validates the panel's persisted state, using the default for missing or invalid values. */
3
+ export function useTabState(tab, schema, defaultState) {
4
+ const parsed = useMemo(() => schema.safeParse(tab.state), [schema, tab.state]);
5
+ const writeState = tab.setState;
6
+ const setState = useCallback((next) => writeState(next), [writeState]);
7
+ return [parsed.success ? parsed.data : defaultState, setState];
8
+ }
9
+ //# sourceMappingURL=tab.js.map
@@ -0,0 +1,42 @@
1
+ import { type ReactNode } from "react";
2
+ import type { PluginMessage, PluginText, PluginTranslations, TranslationKey, TranslationArguments, TranslationResource } from "../text.js";
3
+ export declare function defineTranslations<const Resource extends TranslationResource>(definition: {
4
+ resources: Readonly<Record<string, TranslationResource>> & {
5
+ en: Resource;
6
+ };
7
+ }): PluginTranslations<Resource>;
8
+ /** App language source. Each installation subscribes for exactly its own lifetime. */
9
+ export interface PluginLocaleSource {
10
+ getLocale(): string;
11
+ subscribe(listener: () => void): () => void;
12
+ }
13
+ export interface PluginI18n {
14
+ readonly locale: string;
15
+ t<Resource extends TranslationResource, Key extends TranslationKey<Resource>>(messages: PluginTranslations<Resource>, key: Key, ...args: TranslationArguments<Resource, Key>): string;
16
+ }
17
+ export declare class PluginTranslationRuntime implements PluginI18n {
18
+ private readonly source;
19
+ private readonly engines;
20
+ private readonly listeners;
21
+ private readonly unsubscribe;
22
+ private stopped;
23
+ constructor(source: PluginLocaleSource);
24
+ get locale(): string;
25
+ getSnapshot: () => string;
26
+ subscribe: (listener: () => void) => (() => void);
27
+ t<Resource extends TranslationResource, Key extends TranslationKey<Resource>>(messages: PluginTranslations<Resource>, key: Key, ...args: TranslationArguments<Resource, Key>): string;
28
+ resolve: (text: PluginText, locale?: string) => string;
29
+ dispose(): void;
30
+ }
31
+ export declare function isPluginMessage(value: unknown): value is PluginMessage;
32
+ export declare function requirePluginText(text: unknown): PluginText;
33
+ export declare function PluginTranslationProvider({ runtime, children, }: {
34
+ runtime: PluginTranslationRuntime;
35
+ children: ReactNode;
36
+ }): import("react/jsx-runtime").JSX.Element;
37
+ export declare function usePluginTranslationRuntime(): PluginTranslationRuntime;
38
+ export declare function useTranslation<Resource extends TranslationResource>(messages: PluginTranslations<Resource>): {
39
+ locale: string;
40
+ t: <Key extends TranslationKey<Resource>>(key: Key, ...args: TranslationArguments<Resource, Key>) => string;
41
+ };
42
+ //# sourceMappingURL=translations.d.ts.map
@@ -0,0 +1,153 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext, useCallback, useContext, useSyncExternalStore, } from "react";
3
+ import { createInstance } from "i18next";
4
+ const messageDefinitions = new WeakMap();
5
+ export function defineTranslations(definition) {
6
+ const english = validateResources(definition.resources);
7
+ return {
8
+ resources: definition.resources,
9
+ message: (key, ...args) => {
10
+ const source = english.get(key) ?? english.get(`${key}_other`);
11
+ if (source === undefined)
12
+ throw new Error(`Unknown translation key: ${key}`);
13
+ const options = args[0];
14
+ for (const name of placeholders(source)) {
15
+ if (options?.[name] === undefined)
16
+ throw new Error(`Missing translation value: ${name} (${key})`);
17
+ }
18
+ const message = Object.freeze({});
19
+ messageDefinitions.set(message, { resources: definition.resources, key, options });
20
+ return message;
21
+ },
22
+ };
23
+ }
24
+ export class PluginTranslationRuntime {
25
+ constructor(source) {
26
+ this.source = source;
27
+ this.engines = new Map();
28
+ this.listeners = new Set();
29
+ this.stopped = false;
30
+ this.getSnapshot = () => this.locale;
31
+ this.subscribe = (listener) => {
32
+ if (this.stopped)
33
+ throw new Error("Plugin translations have stopped");
34
+ this.listeners.add(listener);
35
+ return () => {
36
+ this.listeners.delete(listener);
37
+ };
38
+ };
39
+ this.resolve = (text, locale = this.locale) => {
40
+ if (typeof text === "string")
41
+ return text;
42
+ if (this.stopped)
43
+ throw new Error("Plugin translations have stopped");
44
+ const message = messageDefinitions.get(text);
45
+ if (!message)
46
+ throw new Error("Plugin labels must use messages.message()");
47
+ let engine = this.engines.get(message.resources);
48
+ if (!engine) {
49
+ engine = createInstance();
50
+ void engine.init({
51
+ initAsync: false,
52
+ lng: this.locale,
53
+ fallbackLng: "en",
54
+ resources: Object.fromEntries(Object.entries(message.resources).map(([language, translation]) => [
55
+ language,
56
+ { translation },
57
+ ])),
58
+ interpolation: { escapeValue: false },
59
+ returnNull: false,
60
+ returnObjects: false,
61
+ });
62
+ this.engines.set(message.resources, engine);
63
+ }
64
+ return engine.t(message.key, { ...message.options, lng: locale });
65
+ };
66
+ this.unsubscribe = source.subscribe(() => {
67
+ for (const listener of this.listeners)
68
+ listener();
69
+ });
70
+ }
71
+ get locale() {
72
+ return this.source.getLocale();
73
+ }
74
+ t(messages, key, ...args) {
75
+ return this.resolve(messages.message(key, ...args));
76
+ }
77
+ dispose() {
78
+ if (this.stopped)
79
+ return;
80
+ this.stopped = true;
81
+ this.unsubscribe();
82
+ this.listeners.clear();
83
+ this.engines.clear();
84
+ }
85
+ }
86
+ export function isPluginMessage(value) {
87
+ return (typeof value === "object" && value !== null && messageDefinitions.has(value));
88
+ }
89
+ export function requirePluginText(text) {
90
+ if (typeof text === "string" && text.trim())
91
+ return text.trim();
92
+ if (isPluginMessage(text))
93
+ return text;
94
+ throw new Error("Plugin label must be a non-empty string or a translation message");
95
+ }
96
+ function flatten(resource, prefix = "") {
97
+ if (!resource || typeof resource !== "object" || Array.isArray(resource))
98
+ throw new Error("Translation resources must be objects of strings");
99
+ const result = new Map();
100
+ for (const [key, value] of Object.entries(resource)) {
101
+ if (!key || key.includes(".") || ["__proto__", "constructor", "prototype"].includes(key))
102
+ throw new Error(`Invalid translation key: ${key}`);
103
+ const path = prefix ? `${prefix}.${key}` : key;
104
+ if (typeof value === "string")
105
+ result.set(path, value);
106
+ else
107
+ for (const entry of flatten(value, path))
108
+ result.set(...entry);
109
+ }
110
+ return result;
111
+ }
112
+ function pluralBase(key) {
113
+ return key.replace(/_(zero|one|two|few|many|other)$/, "");
114
+ }
115
+ function placeholders(value) {
116
+ return [
117
+ ...new Set([...value.matchAll(/{{-?\s*([^},\s]+)(?:[^}]*)}}/g)].map((match) => match[1])),
118
+ ].sort();
119
+ }
120
+ function validateResources(resources) {
121
+ const english = flatten(resources.en);
122
+ if (!english.size)
123
+ throw new Error("Plugin translations require English messages");
124
+ for (const [locale, resource] of Object.entries(resources)) {
125
+ if (Intl.getCanonicalLocales(locale)[0] !== locale)
126
+ throw new Error(`Translation locale must be canonical: ${locale}`);
127
+ for (const [key, value] of flatten(resource)) {
128
+ const source = english.get(key) ?? english.get(`${pluralBase(key)}_other`);
129
+ if (source === undefined)
130
+ throw new Error(`Unknown translation key in ${locale}: ${key}`);
131
+ if (placeholders(value).join(",") !== placeholders(source).join(","))
132
+ throw new Error(`Translation placeholders differ in ${locale}: ${key}`);
133
+ }
134
+ }
135
+ return english;
136
+ }
137
+ const Context = createContext(null);
138
+ export function PluginTranslationProvider({ runtime, children, }) {
139
+ return _jsx(Context.Provider, { value: runtime, children: children });
140
+ }
141
+ export function usePluginTranslationRuntime() {
142
+ const runtime = useContext(Context);
143
+ if (!runtime)
144
+ throw new Error("Plugin translations must run inside a contributed plugin surface");
145
+ return runtime;
146
+ }
147
+ export function useTranslation(messages) {
148
+ const runtime = usePluginTranslationRuntime();
149
+ const locale = useSyncExternalStore(runtime.subscribe, runtime.getSnapshot, runtime.getSnapshot);
150
+ const t = useCallback((key, ...args) => runtime.resolve(messages.message(key, ...args), locale), [runtime, messages, locale]);
151
+ return { locale, t };
152
+ }
153
+ //# sourceMappingURL=translations.js.map
@@ -1,4 +1,62 @@
1
- import type { ComponentType, ReactNode, Ref } from "react";
1
+ import type { ComponentType, FunctionComponent, ReactNode, ForwardRefExoticComponent, RefAttributes, ReactElement, Ref } from "react";
2
+ import type { StyleProp, ViewStyle, ScrollView as NativeScrollView, ScrollViewProps, FlatList as NativeFlatList, FlatListProps, TextInput as NativeTextInput, TextInputProps, TextProps } from "react-native";
3
+ import type { PluginIconProps } from "../icons.js";
4
+ import type { PluginIcon } from "./contracts.js";
5
+ export interface ModalProps {
6
+ title: string;
7
+ icon?: ReactNode;
8
+ open: boolean;
9
+ onOpenChange(open: boolean): void;
10
+ children: ReactNode;
11
+ }
12
+ export interface ModalContentProps {
13
+ children: ReactNode;
14
+ /** Paint the full body below the header. */
15
+ style?: StyleProp<ViewStyle>;
16
+ /** Overrides the default 24px padding and 16px gap. Safe-area clearance stays host-owned. */
17
+ contentContainerStyle?: StyleProp<ViewStyle>;
18
+ /** Default true. Set false for a bounded body with your own ScrollView or FlatList. */
19
+ scrollable?: boolean;
20
+ }
21
+ export interface ModalComponent extends FunctionComponent<ModalProps> {
22
+ Content: ComponentType<ModalContentProps>;
23
+ }
24
+ export type ToastVariant = "default" | "info" | "success" | "warning" | "error";
25
+ export interface ToastOptions {
26
+ variant?: ToastVariant;
27
+ durationMs?: number;
28
+ }
29
+ export interface ToastApi {
30
+ show(message: string, options?: ToastOptions): void;
31
+ error(message: string): void;
32
+ }
33
+ /** Renders a Lucide icon by `name`, or any plugin icon by `icon`. An unusable icon renders nothing. */
34
+ export type IconProps = Partial<PluginIconProps> & ({
35
+ name: string;
36
+ icon?: never;
37
+ } | {
38
+ icon: PluginIcon;
39
+ name?: never;
40
+ });
41
+ export declare const Icon: ComponentType<IconProps>;
42
+ export declare const Modal: ModalComponent;
43
+ export declare function useToast(): ToastApi;
44
+ export interface StreamedTextProps extends Omit<TextProps, "children"> {
45
+ text: string;
46
+ phase: "streaming" | "complete";
47
+ }
48
+ /** Plain assistant text with host-owned streaming pacing and typography. */
49
+ export declare const StreamedText: ComponentType<StreamedTextProps>;
50
+ export type { PluginIconProps } from "../icons.js";
51
+ /** React Native scrolling with the host's sheet gestures when rendered inside a sheet. */
52
+ export declare const ScrollView: ForwardRefExoticComponent<ScrollViewProps & RefAttributes<NativeScrollView>>;
53
+ export declare function FlatList<Item>(props: FlatListProps<Item> & {
54
+ ref?: Ref<NativeFlatList<Item>>;
55
+ }): ReactElement;
56
+ /** Copies text to this client's clipboard. Rejects when copying is unavailable or denied. */
57
+ export declare function copyText(text: string): Promise<void>;
58
+ /** Native input focus integrated with modal keyboard positioning. */
59
+ export declare const TextInput: ForwardRefExoticComponent<TextInputProps & RefAttributes<NativeTextInput>>;
2
60
  export interface SettingsSectionProps {
3
61
  title: string;
4
62
  info?: ReactNode;
@@ -46,6 +104,7 @@ export interface SettingsActionProps extends SettingsRowProps {
46
104
  onPress(): void;
47
105
  disabled?: boolean;
48
106
  }
107
+ /** @deprecated Use SettingsSection for plugin settings headings. */
49
108
  export declare const SettingsGroup: ComponentType<SettingsSectionProps>;
50
109
  export declare const SettingsSection: ComponentType<SettingsSectionProps>;
51
110
  export declare const SettingsCard: ComponentType<{
@@ -65,17 +124,14 @@ export interface ExternalLinkProps {
65
124
  onError?: (error: unknown) => void;
66
125
  }
67
126
  export declare const ExternalLink: ComponentType<ExternalLinkProps>;
68
- export type SidebarIcon = string | ComponentType<{
69
- size: number;
70
- color: string;
71
- }>;
127
+ export type SidebarIcon = PluginIcon;
72
128
  export interface SidebarRowProps {
73
129
  /**
74
130
  * Tells rows of one item apart when the item renders several: "bot-2". Unique within the item.
75
131
  * Omit it when the item renders one row.
76
132
  */
77
133
  id?: string;
78
- /** A Lucide icon name or a component. */
134
+ /** A Lucide icon name, inline SVG, or a component. */
79
135
  icon?: SidebarIcon;
80
136
  /** Defaults to the item's registered title. */
81
137
  label?: string;
@@ -88,4 +144,16 @@ export interface SidebarRowProps {
88
144
  export declare const SidebarRow: ComponentType<SidebarRowProps>;
89
145
  /** A line between groups of rows. Render it from a sidebar item's `Component`. */
90
146
  export declare const SidebarSeparator: ComponentType;
147
+ /** Paseo's loading state. Fills the panel; the panel decides when to show it. */
148
+ export declare const Loading: ComponentType;
149
+ /** Render a local document URL in an isolated frame. */
150
+ export declare const DocumentFrame: ComponentType<{
151
+ url: string;
152
+ testID?: string;
153
+ }>;
154
+ export interface MarkdownProps {
155
+ children: string;
156
+ }
157
+ /** Renders Markdown using Paseo’s theme, selection, and link behavior. */
158
+ export declare const Markdown: ComponentType<MarkdownProps>;
91
159
  //# sourceMappingURL=ui.d.ts.map
@@ -1,4 +1,6 @@
1
+ import type { PluginText } from "./text.js";
1
2
  import type { JsonValue } from "@camerontaylor/paseo-protocol/agent-types";
3
+ import type { PluginSerializableIcon } from "./icons.js";
2
4
  import type { PluginRpcContract } from "./rpc.js";
3
5
  export interface PluginTheme {
4
6
  readonly colors: {
@@ -61,18 +63,18 @@ export interface PluginThemeColors {
61
63
  mutedForeground: string;
62
64
  ring: string;
63
65
  }
64
- export interface PluginThemeContribution {
66
+ export interface PluginThemeContribution<Text extends PluginText = string> {
65
67
  id: string;
66
- name: string;
68
+ name: Text;
67
69
  appearance: "light" | "dark";
68
70
  colors: PluginThemeColors;
69
71
  }
70
- export interface PluginAttachmentSourceContribution {
72
+ export interface PluginAttachmentSourceContribution<Text extends PluginText = string> {
71
73
  id: string;
72
- title: string;
73
- icon: string;
74
- pickerTitle: string;
75
- searchPlaceholder: string;
74
+ title: Text;
75
+ icon: PluginSerializableIcon;
76
+ pickerTitle: Text;
77
+ searchPlaceholder: Text;
76
78
  search: PluginRpcContract;
77
79
  }
78
80
  export type PluginTimelineData = JsonValue;
@@ -0,0 +1,18 @@
1
+ /** What a component icon receives from the surface that draws it. */
2
+ export interface PluginIconProps {
3
+ size: number;
4
+ color: string;
5
+ }
6
+ /** Inline SVG markup. Paseo validates it when the contribution registers. */
7
+ export interface PluginSvgIcon {
8
+ svg: string;
9
+ }
10
+ /** An icon that can be stored or sent: a Lucide icon name, or inline SVG. */
11
+ export type PluginSerializableIcon = string | PluginSvgIcon;
12
+ export declare const PLUGIN_SVG_ICON_MAX_BYTES: number;
13
+ /**
14
+ * Why Paseo refuses to render this SVG, or null when it is a self-contained document.
15
+ * The daemon applies it to provider and usage-source icon files; the app to inline plugin icons.
16
+ */
17
+ export declare function findSvgIconProblem(svg: string): string | null;
18
+ //# sourceMappingURL=icons.d.ts.map
package/dist/icons.js ADDED
@@ -0,0 +1,30 @@
1
+ export const PLUGIN_SVG_ICON_MAX_BYTES = 64 * 1024;
2
+ const EXTERNAL_HREF = /\s(?:href|xlink:href)\s*=\s*(?:(["'])(.*?)\1|([^\s>]+))/gi;
3
+ /**
4
+ * Why Paseo refuses to render this SVG, or null when it is a self-contained document.
5
+ * The daemon applies it to provider and usage-source icon files; the app to inline plugin icons.
6
+ */
7
+ export function findSvgIconProblem(svg) {
8
+ if (new TextEncoder().encode(svg).byteLength > PLUGIN_SVG_ICON_MAX_BYTES) {
9
+ return "exceeds 64 KiB";
10
+ }
11
+ if (!/^\s*<svg(?:\s|>)/i.test(svg))
12
+ return "not an SVG document";
13
+ if (/<script(?:\s|>)/i.test(svg))
14
+ return "script elements are not allowed";
15
+ if (/<foreignObject(?:\s|>)/i.test(svg))
16
+ return "foreignObject elements are not allowed";
17
+ if (/<style(?:\s|>)/i.test(svg))
18
+ return "style elements are not allowed";
19
+ if (/\son[a-z0-9_-]*\s*=/i.test(svg))
20
+ return "event-handler attributes are not allowed";
21
+ if (/javascript\s*:/i.test(svg))
22
+ return "javascript URLs are not allowed";
23
+ for (const match of svg.matchAll(EXTERNAL_HREF)) {
24
+ const href = match[2] ?? match[3] ?? "";
25
+ if (!href.startsWith("#"))
26
+ return "external href references are not allowed";
27
+ }
28
+ return null;
29
+ }
30
+ //# sourceMappingURL=icons.js.map
package/dist/index.d.ts CHANGED
@@ -3,4 +3,6 @@ export type { PluginTheme, PluginWorkspaceSnapshot, PluginAgentSnapshot, PluginT
3
3
  export { defineSettings, settingsRpc, type SettingsDefinition } from "./settings.js";
4
4
  export { defineAttachmentSource, PluginAttachmentItemSchema, PluginAttachmentSearchPayloadSchema, type PluginAttachmentItem, type PluginAttachmentSearchPayload, } from "./attachments.js";
5
5
  export { defineRpc, type PluginRpcContract, type RpcInput, type RpcOutput } from "./rpc.js";
6
+ export type { PluginText, PluginMessage } from "./text.js";
7
+ export { findSvgIconProblem, PLUGIN_SVG_ICON_MAX_BYTES, type PluginIconProps, type PluginSerializableIcon, type PluginSvgIcon, } from "./icons.js";
6
8
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -3,4 +3,5 @@ export { PLUGIN_SDK_PACKAGE_NAMES, pluginSdkEntry } from "./sdk-specifiers.js";
3
3
  export { defineSettings, settingsRpc } from "./settings.js";
4
4
  export { defineAttachmentSource, PluginAttachmentItemSchema, PluginAttachmentSearchPayloadSchema, } from "./attachments.js";
5
5
  export { defineRpc } from "./rpc.js";
6
+ export { findSvgIconProblem, PLUGIN_SVG_ICON_MAX_BYTES, } from "./icons.js";
6
7
  //# sourceMappingURL=index.js.map
@@ -319,10 +319,16 @@ class AcpRuntime {
319
319
  /**
320
320
  * Fallback ids for streamed chunks that carry no `messageId`, one per
321
321
  * chunk kind. Consecutive chunks of a kind continue the same timeline
322
- * item; a chunk of the other kind, a tool call, a plan, a user message,
323
- * or a turn boundary ends it (#4699).
322
+ * item; a chunk of another kind, a tool call, a plan, or a turn boundary
323
+ * ends it (#4699).
324
324
  */
325
325
  this.fallbackChunkIds = new Map();
326
+ /**
327
+ * Active while a persisted session loads. ACP agents replay the loaded
328
+ * session's updates before session/load resolves; the value follows the
329
+ * open request's history contract.
330
+ */
331
+ this.replayWindow = null;
326
332
  this.closing = false;
327
333
  this.processFailed = false;
328
334
  this.configTransaction = false;
@@ -377,13 +383,22 @@ class AcpRuntime {
377
383
  };
378
384
  let response;
379
385
  if (nativeSessionId) {
380
- response = await this.call(this.connection.loadSession({
381
- sessionId: nativeSessionId,
382
- cwd: input.config.cwd,
383
- mcpServers,
384
- _meta: metadata,
385
- }));
386
+ // ACP agents replay the loaded session's updates before session/load resolves,
387
+ // so the native id must be registered first for them to pass the session filter.
386
388
  this.nativeSessionId = nativeSessionId;
389
+ this.replayWindow = input.history === "replay" ? "replay" : "skip";
390
+ try {
391
+ response = await this.call(this.connection.loadSession({
392
+ sessionId: nativeSessionId,
393
+ cwd: input.config.cwd,
394
+ mcpServers,
395
+ _meta: metadata,
396
+ }));
397
+ await this.drainNotifications();
398
+ }
399
+ finally {
400
+ this.replayWindow = null;
401
+ }
387
402
  }
388
403
  else {
389
404
  const newSession = await this.call(this.connection.newSession({
@@ -732,6 +747,13 @@ class AcpRuntime {
732
747
  sessionUpdate(notification) {
733
748
  if (notification.sessionId !== this.nativeSessionId)
734
749
  return;
750
+ if (this.replayWindow === "skip")
751
+ return;
752
+ if (this.replayWindow === "replay" &&
753
+ notification.update.sessionUpdate === "user_message_chunk") {
754
+ this.reduceUserMessageChunk(notification.update);
755
+ return;
756
+ }
735
757
  this.reduceUpdate(notification.update);
736
758
  }
737
759
  reduceUpdate(update) {
@@ -742,11 +764,12 @@ class AcpRuntime {
742
764
  this.fallbackChunkIds.clear();
743
765
  return;
744
766
  }
745
- // A chunk of one kind ends the other kind's item, whatever its content
746
- // or id: reasoning, then text, then reasoning again are three items.
767
+ // A chunk of one kind ends every other kind's open item, whatever its
768
+ // content or id: reasoning, then text, then reasoning again are three items.
747
769
  this.fallbackChunkIds.delete(update.sessionUpdate === "agent_message_chunk"
748
770
  ? "agent_thought_chunk"
749
771
  : "agent_message_chunk");
772
+ this.fallbackChunkIds.delete("user_message_chunk");
750
773
  if (update.content.type !== "text")
751
774
  return;
752
775
  const id = this.resolveChunkId(update.sessionUpdate, update.messageId);
@@ -776,6 +799,27 @@ class AcpRuntime {
776
799
  this.fallbackChunkIds.clear();
777
800
  this.reduceStateUpdate(update);
778
801
  }
802
+ // Historical user messages arrive only as replayed chunks; a live prompt
803
+ // already echoes its own user message when it is admitted.
804
+ reduceUserMessageChunk(update) {
805
+ this.fallbackChunkIds.delete("agent_message_chunk");
806
+ this.fallbackChunkIds.delete("agent_thought_chunk");
807
+ if (update.content.type !== "text")
808
+ return;
809
+ const id = this.resolveChunkId("user_message_chunk", update.messageId);
810
+ const text = `${this.messages.get(id) ?? ""}${update.content.text}`;
811
+ this.messages.set(id, text);
812
+ this.emit({
813
+ type: "timeline.item",
814
+ sessionId: this.options.boundarySessionId,
815
+ item: {
816
+ type: "user_message",
817
+ id,
818
+ text,
819
+ ...(update.messageId ? { messageId: update.messageId } : {}),
820
+ },
821
+ });
822
+ }
779
823
  resolveChunkId(kind, messageId) {
780
824
  if (messageId) {
781
825
  this.fallbackChunkIds.delete(kind);
@@ -50,6 +50,10 @@ export interface PluginLifecycleEvents {
50
50
  agent: PluginHookAgent;
51
51
  turnId: string | null;
52
52
  outcome: PluginTurnOutcome;
53
+ /**
54
+ * @deprecated Always empty. Fetch history on demand with
55
+ * context.paseo.agents.ref(agent.id).timeline.refetch({ direction: "tail", limit: 20 }).
56
+ */
53
57
  timeline: readonly AgentTimelineItem[];
54
58
  };
55
59
  "agent.permission_requested": {
package/dist/text.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ /** A persistent label resolved in the presenting app's language. */
2
+ declare const pluginMessage: unique symbol;
3
+ export interface PluginMessage {
4
+ readonly [pluginMessage]: true;
5
+ }
6
+ export type PluginText = string | PluginMessage;
7
+ export interface TranslationResource {
8
+ readonly [key: string]: string | TranslationResource;
9
+ }
10
+ export interface TranslationOptions {
11
+ readonly count?: number;
12
+ readonly [name: string]: unknown;
13
+ }
14
+ export type TranslationKey<Resource> = {
15
+ [Key in keyof Resource & string]: Resource[Key] extends string ? Key | (Key extends `${infer Base}_${"zero" | "one" | "two" | "few" | "many" | "other"}` ? Base : never) : Resource[Key] extends TranslationResource ? `${Key}.${TranslationKey<Resource[Key]>}` : never;
16
+ }[keyof Resource & string];
17
+ export interface PluginTranslations<Resource extends TranslationResource = TranslationResource> {
18
+ readonly resources: Readonly<Record<string, TranslationResource>> & {
19
+ readonly en: Resource;
20
+ };
21
+ message<Key extends TranslationKey<Resource>>(key: Key, ...args: TranslationArguments<Resource, Key>): PluginMessage;
22
+ }
23
+ type Lookup<Resource, Key extends string> = Key extends `${infer Head}.${infer Tail}` ? Head extends keyof Resource ? Lookup<Resource[Head], Tail> : never : Key extends keyof Resource ? Resource[Key] : never;
24
+ type Trim<Value extends string> = Value extends ` ${infer Rest}` | `${infer Rest} ` ? Trim<Rest> : Value;
25
+ type InterpolationName<Value extends string> = Trim<Value> extends `-${infer Rest}` ? InterpolationName<Rest> : Trim<Value> extends `${infer Name},${string}` ? Trim<Name> : Trim<Value>;
26
+ type InterpolationNames<Value> = Value extends string ? string extends Value ? never : Value extends `${string}{{${infer Name}}}${infer Rest}` ? InterpolationName<Name> | InterpolationNames<Rest> : never : never;
27
+ type MessageOptions<Resource, Key extends string> = TranslationOptions & ([Lookup<Resource, `${Key}_other`>] extends [never] ? {} : {
28
+ count: number;
29
+ }) & {
30
+ [Name in InterpolationNames<Lookup<Resource, Key> | Lookup<Resource, `${Key}_other`>>]: string | number | boolean | Date;
31
+ };
32
+ export type TranslationArguments<Resource, Key extends string> = {} extends MessageOptions<Resource, Key> ? [options?: MessageOptions<Resource, Key>] : [options: MessageOptions<Resource, Key>];
33
+ export {};
34
+ //# sourceMappingURL=text.d.ts.map
package/dist/text.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=text.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camerontaylor/paseo-plugin",
3
- "version": "0.12.0-fork.1",
3
+ "version": "0.13.0-fork.2",
4
4
  "description": "Paseo plugin SDK package",
5
5
  "files": [
6
6
  "dist",
@@ -62,28 +62,30 @@
62
62
  "build": "tsc -p tsconfig.json --incremental false",
63
63
  "build:clean": "npm run clean && npm run build",
64
64
  "prepack": "npm run build:clean",
65
- "typecheck": "tsgo --noEmit && tsgo -p tsconfig.examples.json --noEmit",
66
- "test": "vitest run"
65
+ "typecheck": "tsgo --noEmit && npm run typecheck:examples",
66
+ "test": "vitest run",
67
+ "typecheck:examples": "npm --prefix ../../plugin-examples/markdown-panel install --ignore-scripts --no-audit --no-fund && tsgo -p tsconfig.examples.json --noEmit"
67
68
  },
68
69
  "dependencies": {
69
70
  "@agentclientprotocol/sdk": "^1.4.0",
71
+ "i18next": "^26.3.0",
70
72
  "use-sync-external-store": "^1.6.0"
71
73
  },
72
74
  "devDependencies": {
73
- "@camerontaylor/paseo-client": "0.12.0-fork.1",
74
- "@camerontaylor/paseo-protocol": "0.12.0-fork.1",
75
+ "@camerontaylor/paseo-client": "0.13.0-fork.2",
76
+ "@camerontaylor/paseo-protocol": "0.13.0-fork.2",
75
77
  "@types/node": "^20.9.0",
76
78
  "@types/react": "~19.2.0",
77
- "react": "19.1.0",
78
- "react-native": "0.81.5",
79
+ "react": "19.2.3",
80
+ "react-native": "0.86.3",
79
81
  "typescript": "^5.9.3",
80
82
  "vitest": "^4.1.6",
81
83
  "zod": "^4.4.3"
82
84
  },
83
85
  "peerDependencies": {
84
- "@camerontaylor/paseo-client": "0.12.0-fork.1",
85
- "@camerontaylor/paseo-protocol": "0.12.0-fork.1",
86
- "react": "~19.1.0",
86
+ "@camerontaylor/paseo-client": "0.13.0-fork.2",
87
+ "@camerontaylor/paseo-protocol": "0.13.0-fork.2",
88
+ "react": ">=19.1.0 <20",
87
89
  "react-native": ">=0.81.5",
88
90
  "zod": "^4.4.3"
89
91
  },