@xpufx/paseo-top 0.4.0 → 0.4.1
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/client/telemetry-copy.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatUptime } from "paseo-plugin-helper/shared";
|
|
1
|
+
import { formatBytes, formatUptime } from "paseo-plugin-helper/shared";
|
|
2
2
|
import { formatCompactTokens, type TopAgentSnapshot } from "./pill-labels";
|
|
3
3
|
import type { TopTimelineTelemetryData } from "../shared/resources";
|
|
4
4
|
|
|
@@ -55,9 +55,16 @@ export function buildTelemetryCopyText({ data, liveUsage, timeLabel }: Telemetry
|
|
|
55
55
|
const time = timeLabel ?? data.timestamp;
|
|
56
56
|
if (time) lines.push(time);
|
|
57
57
|
|
|
58
|
+
// `turnId` is normally an opaque id, but some providers use the source
|
|
59
|
+
// issue URL. Keep it as a labeled reference: it is useful context, but it
|
|
60
|
+
// must never become the whole clipboard payload.
|
|
61
|
+
if (data.turnId) lines.push(`Reference ${data.turnId}`);
|
|
62
|
+
|
|
58
63
|
const id = shortAgentId(data.agentId);
|
|
59
64
|
if (id) lines.push(`Agent ${id}`);
|
|
60
65
|
|
|
66
|
+
if (data.agentTitle) lines.push(`Title ${data.agentTitle}`);
|
|
67
|
+
|
|
61
68
|
const model = data.agentModel ?? undefined;
|
|
62
69
|
const provider = data.agentProvider ?? undefined;
|
|
63
70
|
if (model || provider) {
|
|
@@ -67,6 +74,13 @@ export function buildTelemetryCopyText({ data, liveUsage, timeLabel }: Telemetry
|
|
|
67
74
|
if (data.branch) lines.push(`Branch ${data.branch}`);
|
|
68
75
|
if (data.worktree) lines.push(`Worktree ${data.worktree}`);
|
|
69
76
|
|
|
77
|
+
// These are the first row of the rendered card body. Keep their labels and
|
|
78
|
+
// formatting aligned with the card so Copy is a human-readable export of
|
|
79
|
+
// what the operator sees, rather than just an identifier or URL.
|
|
80
|
+
lines.push(`CPU ${data.cpuPercent}%`);
|
|
81
|
+
lines.push(`RAM ${formatBytes(data.memUsedBytes)} (${data.memPercent}%)`);
|
|
82
|
+
lines.push(`Load ${data.loadAvg1m.toFixed(2)}`);
|
|
83
|
+
|
|
70
84
|
const totalTokens =
|
|
71
85
|
inputTokens != null || outputTokens != null
|
|
72
86
|
? (inputTokens ?? 0) + (outputTokens ?? 0)
|
|
@@ -24,16 +24,23 @@ export interface ClipboardEnvironment {
|
|
|
24
24
|
* `react-native-web` `Clipboard.setString` reports success even when its
|
|
25
25
|
* `document.execCommand("copy")` fails, which leaves the previous clipboard
|
|
26
26
|
* item in place while the UI claims success (xpufx-org/paseo#278); it is only
|
|
27
|
-
* usable off-DOM (native), where it is the real platform clipboard.
|
|
28
|
-
*
|
|
27
|
+
* usable off-DOM (native), where it is the real platform clipboard. The same
|
|
28
|
+
* applies to any `setStringAsync` whose DOM fallback is that unverified
|
|
29
|
+
* `execCommand`. In a DOM the checked `execCommand` fallback is preferred to
|
|
30
|
+
* those unverifiable paths.
|
|
29
31
|
*/
|
|
30
32
|
export function clipboardTierOrder(env: ClipboardEnvironment): ClipboardTier[] {
|
|
31
33
|
const tiers: ClipboardTier[] = [];
|
|
32
34
|
if (env.hasNavigatorClipboard) tiers.push("navigator");
|
|
33
35
|
if (env.hasHostCopyText) tiers.push("host");
|
|
34
|
-
|
|
36
|
+
// On a DOM both RN-web paths are unverifiable: `setString` reports success
|
|
37
|
+
// even when its `execCommand` no-ops, and a `setStringAsync` that falls back
|
|
38
|
+
// to it does the same. Off-DOM they are the real native clipboard, so they
|
|
39
|
+
// lead there; on a DOM only the checked `execCommand` fallback is honest.
|
|
40
|
+
const rnDomOk = !env.isDom;
|
|
41
|
+
if (env.hasRnSetStringAsync && rnDomOk) {
|
|
35
42
|
tiers.push("rnAsync");
|
|
36
|
-
} else if (env.hasRnClipboard &&
|
|
43
|
+
} else if (env.hasRnClipboard && rnDomOk) {
|
|
37
44
|
tiers.push("rnSync");
|
|
38
45
|
}
|
|
39
46
|
tiers.push("execCommand");
|
package/index.client.tsx
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { PluginClientContext } from "@getpaseo/plugin/client";
|
|
2
|
+
import { useRpc } from "@getpaseo/plugin/client";
|
|
3
|
+
import {
|
|
4
|
+
Icon,
|
|
5
|
+
Modal,
|
|
6
|
+
useToast,
|
|
7
|
+
ScrollView,
|
|
8
|
+
FlatList,
|
|
9
|
+
TextInput as HostTextInput,
|
|
10
|
+
copyText,
|
|
11
|
+
} from "@getpaseo/plugin/client/react-native";
|
|
12
|
+
import { initClientHelpers } from "paseo-plugin-helper/client";
|
|
13
|
+
import { contributeClient } from "./client/pill";
|
|
14
|
+
import { TopTimelineTelemetryCard } from "./client/telemetry";
|
|
15
|
+
import { TurnCounterPanel } from "./client/turn-panel";
|
|
16
|
+
import {
|
|
17
|
+
TOP_TIMELINE_KIND,
|
|
18
|
+
TOP_TIMELINE_VERSION,
|
|
19
|
+
topTimelineTelemetrySchema,
|
|
20
|
+
} from "./shared/resources";
|
|
21
|
+
|
|
22
|
+
initClientHelpers({
|
|
23
|
+
Icon,
|
|
24
|
+
Modal,
|
|
25
|
+
useRpc,
|
|
26
|
+
useToast,
|
|
27
|
+
copyText,
|
|
28
|
+
ScrollView,
|
|
29
|
+
FlatList,
|
|
30
|
+
TextInput: HostTextInput,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export default function contribute(client: PluginClientContext) {
|
|
34
|
+
const removeTimelineRenderer = client.addTimelineRenderer({
|
|
35
|
+
kind: TOP_TIMELINE_KIND,
|
|
36
|
+
version: TOP_TIMELINE_VERSION,
|
|
37
|
+
schema: topTimelineTelemetrySchema,
|
|
38
|
+
Component: TopTimelineTelemetryCard,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const cleanupClient = contributeClient(client);
|
|
42
|
+
|
|
43
|
+
// Agent-scoped panel: exposes the deterministic turn index for the active
|
|
44
|
+
// conversation (Turn #N + canonical seq) so a human and the agent resolve the
|
|
45
|
+
// same reference. See client/turn-counter.ts. Agent panels are opened through
|
|
46
|
+
// a command-center item, which forwards the active agent's openPanel.
|
|
47
|
+
const removeTurnPanel = client.addWorkspacePanel({
|
|
48
|
+
id: "top-turn-counter",
|
|
49
|
+
title: "Turn Counter",
|
|
50
|
+
icon: "Repeat",
|
|
51
|
+
context: "agent",
|
|
52
|
+
Component: TurnCounterPanel,
|
|
53
|
+
});
|
|
54
|
+
const removeTurnCommand = client.addCommandCenterItem({
|
|
55
|
+
id: "top-turn-counter",
|
|
56
|
+
title: "Turn Counter",
|
|
57
|
+
icon: "Repeat",
|
|
58
|
+
keywords: ["turn", "seq", "counter", "conversation"],
|
|
59
|
+
context: "agent",
|
|
60
|
+
onSelect(ctx) {
|
|
61
|
+
ctx.openPanel("top-turn-counter");
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return () => {
|
|
66
|
+
removeTurnCommand();
|
|
67
|
+
removeTurnPanel();
|
|
68
|
+
removeTimelineRenderer();
|
|
69
|
+
cleanupClient();
|
|
70
|
+
};
|
|
71
|
+
}
|
package/index.server.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import type { PluginServerContext } from "@getpaseo/plugin/server";
|
|
2
|
+
import { guardRpcHandler } from "paseo-plugin-helper/server";
|
|
3
|
+
import {
|
|
4
|
+
getSystemResourcesRpc,
|
|
5
|
+
getCustomPillsRpc,
|
|
6
|
+
listCustomPillsRpc,
|
|
7
|
+
runCustomPillModalCommandRpc,
|
|
8
|
+
topSettingsContract,
|
|
9
|
+
TOP_TIMELINE_KIND,
|
|
10
|
+
TOP_TIMELINE_VERSION,
|
|
11
|
+
type LiveUsage,
|
|
12
|
+
type SystemResources,
|
|
13
|
+
} from "./shared/resources";
|
|
14
|
+
import {
|
|
15
|
+
handleGetSystemResources,
|
|
16
|
+
handleGetCustomPills,
|
|
17
|
+
handleListCustomPills,
|
|
18
|
+
handleRunCustomPillModalCommand,
|
|
19
|
+
handleGetSettings,
|
|
20
|
+
handleUpdateSettings,
|
|
21
|
+
handleResetSettings,
|
|
22
|
+
customPillPoller,
|
|
23
|
+
collectTurnTelemetry,
|
|
24
|
+
collectGitDiffStat,
|
|
25
|
+
countTurns,
|
|
26
|
+
getLastLiveUsage,
|
|
27
|
+
isInterruptEcho,
|
|
28
|
+
isStaleTurnEnd,
|
|
29
|
+
setLastLiveUsage,
|
|
30
|
+
log,
|
|
31
|
+
} from "./server/resources";
|
|
32
|
+
import { resolveTimelineCadence, shouldAppendTimelineForTurn } from "./shared/resources";
|
|
33
|
+
|
|
34
|
+
export default function contribute(server: PluginServerContext) {
|
|
35
|
+
void customPillPoller.start();
|
|
36
|
+
|
|
37
|
+
// Shed load instead of hanging the daemon RPC: saturated or slow handlers
|
|
38
|
+
// answer from the last good snapshot (system-resources) or fail fast.
|
|
39
|
+
// WARNs are rate-limited (one per minute per cause): saturation fires once
|
|
40
|
+
// per poll tick per caller while the modal is open, which flooded the log.
|
|
41
|
+
let lastSystemResources: SystemResources | null = null;
|
|
42
|
+
const lastWarnAt = { timeout: 0, saturated: 0 };
|
|
43
|
+
const WARN_COOLDOWN_MS = 60_000;
|
|
44
|
+
const guardedSystemResources = guardRpcHandler(
|
|
45
|
+
async (input: Parameters<typeof handleGetSystemResources>[0]) => {
|
|
46
|
+
const resources = await handleGetSystemResources(input);
|
|
47
|
+
lastSystemResources = resources;
|
|
48
|
+
return resources;
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
timeoutMs: 5000,
|
|
52
|
+
maxInflight: 4,
|
|
53
|
+
getStale: () => lastSystemResources,
|
|
54
|
+
onTimeout: ({ timeoutMs }) => {
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
if (now - lastWarnAt.timeout < WARN_COOLDOWN_MS) return;
|
|
57
|
+
lastWarnAt.timeout = now;
|
|
58
|
+
log.warn("system-resources handler timed out", { timeoutMs });
|
|
59
|
+
},
|
|
60
|
+
onSaturated: ({ maxInflight }) => {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
if (now - lastWarnAt.saturated < WARN_COOLDOWN_MS) return;
|
|
63
|
+
lastWarnAt.saturated = now;
|
|
64
|
+
log.warn("system-resources handler saturated, serving stale", { maxInflight });
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
server.handle(topSettingsContract.get, handleGetSettings);
|
|
70
|
+
server.handle(topSettingsContract.update, handleUpdateSettings);
|
|
71
|
+
server.handle(topSettingsContract.reset, handleResetSettings);
|
|
72
|
+
server.handle(getSystemResourcesRpc, guardedSystemResources);
|
|
73
|
+
server.handle(getCustomPillsRpc, handleGetCustomPills);
|
|
74
|
+
server.handle(listCustomPillsRpc, handleListCustomPills);
|
|
75
|
+
server.handle(runCustomPillModalCommandRpc, handleRunCustomPillModalCommand);
|
|
76
|
+
|
|
77
|
+
const turnStartTimes = new Map<string, number>();
|
|
78
|
+
const turnGitBefore = new Map<string, { insertions: number; deletions: number; filesChanged: number }>();
|
|
79
|
+
// Live turn per agent, set on turn_started and consumed by its terminal, used
|
|
80
|
+
// to drop terminals that cannot belong to it (see isStaleTurnEnd).
|
|
81
|
+
const activeTurnIds = new Map<string, string | null>();
|
|
82
|
+
// Time and timeline user_message count of the last canceled terminal, used to
|
|
83
|
+
// drop the daemon's extra interrupt terminal (see isInterruptEcho).
|
|
84
|
+
const lastCanceledAt = new Map<string, number>();
|
|
85
|
+
const lastCanceledUserMessages = new Map<string, number>();
|
|
86
|
+
// Per-agent turn counter for the timeline cadence option (0 = never,
|
|
87
|
+
// 1 = every turn, N>1 = every Nth turn). Counts deduped turn_ended events.
|
|
88
|
+
const turnCounters = new Map<string, number>();
|
|
89
|
+
|
|
90
|
+
const unsubscribeTurnStarted = server.on("agent.turn_started", (event, context) => {
|
|
91
|
+
turnStartTimes.set(event.agent.id, Date.now());
|
|
92
|
+
activeTurnIds.set(event.agent.id, event.turnId ?? null);
|
|
93
|
+
if ((event.agent as any)?.lastUsage) {
|
|
94
|
+
setLastLiveUsage((event.agent as any).lastUsage);
|
|
95
|
+
}
|
|
96
|
+
void context.paseo.agents
|
|
97
|
+
.ref(event.agent.id)
|
|
98
|
+
.refresh()
|
|
99
|
+
.then((refetched) => {
|
|
100
|
+
if (refetched && (refetched.agent as any)?.lastUsage) {
|
|
101
|
+
setLastLiveUsage((refetched.agent as any).lastUsage);
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
.catch(() => {});
|
|
105
|
+
void collectGitDiffStat(event.agent.cwd).then(
|
|
106
|
+
(before) => {
|
|
107
|
+
if (before) {
|
|
108
|
+
turnGitBefore.set(event.agent.id, before);
|
|
109
|
+
} else {
|
|
110
|
+
turnGitBefore.delete(event.agent.id);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
() => {
|
|
114
|
+
turnGitBefore.delete(event.agent.id);
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const unsubscribeAgentCreated = server.on("agent.created", (event) => {
|
|
120
|
+
if ((event.agent as any)?.lastUsage) {
|
|
121
|
+
setLastLiveUsage((event.agent as any).lastUsage);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
async function appendTurnCard(
|
|
126
|
+
event: any,
|
|
127
|
+
context: any,
|
|
128
|
+
startTime: number | undefined,
|
|
129
|
+
gitBefore: { insertions: number; deletions: number; filesChanged: number } | undefined,
|
|
130
|
+
turnIndex: number,
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
try {
|
|
133
|
+
const settings = await handleGetSettings();
|
|
134
|
+
const cadence = resolveTimelineCadence(settings);
|
|
135
|
+
if (!shouldAppendTimelineForTurn(cadence, turnIndex)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const durationMs = startTime ? Date.now() - startTime : undefined;
|
|
139
|
+
|
|
140
|
+
let agentModel: string | null = null;
|
|
141
|
+
let agentProvider: string | null = event.agent.provider ?? null;
|
|
142
|
+
let agentTitle: string | null = event.agent.title ?? null;
|
|
143
|
+
try {
|
|
144
|
+
const refetched = await context.paseo.agents.ref(event.agent.id).refresh();
|
|
145
|
+
agentModel = refetched?.agent?.model ?? agentModel;
|
|
146
|
+
agentProvider = refetched?.agent?.provider ?? agentProvider;
|
|
147
|
+
agentTitle = refetched?.agent?.title ?? agentTitle;
|
|
148
|
+
let liveUsage =
|
|
149
|
+
(refetched?.agent?.lastUsage as LiveUsage | null | undefined) ??
|
|
150
|
+
((event.agent as any)?.lastUsage as LiveUsage | null | undefined) ??
|
|
151
|
+
null;
|
|
152
|
+
const lacksTokens =
|
|
153
|
+
liveUsage?.inputTokens == null &&
|
|
154
|
+
liveUsage?.outputTokens == null &&
|
|
155
|
+
(liveUsage as any)?.contextWindowUsedTokens == null;
|
|
156
|
+
if (lacksTokens) {
|
|
157
|
+
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
158
|
+
await sleep(150);
|
|
159
|
+
try {
|
|
160
|
+
const retry1 = await context.paseo.agents.ref(event.agent.id).refresh();
|
|
161
|
+
liveUsage =
|
|
162
|
+
(retry1?.agent?.lastUsage as LiveUsage | null | undefined) ?? liveUsage;
|
|
163
|
+
} catch {}
|
|
164
|
+
const stillLacks =
|
|
165
|
+
liveUsage?.inputTokens == null &&
|
|
166
|
+
liveUsage?.outputTokens == null &&
|
|
167
|
+
(liveUsage as any)?.contextWindowUsedTokens == null;
|
|
168
|
+
if (stillLacks) {
|
|
169
|
+
await sleep(250);
|
|
170
|
+
try {
|
|
171
|
+
const retry2 = await context.paseo.agents.ref(event.agent.id).refresh();
|
|
172
|
+
liveUsage =
|
|
173
|
+
(retry2?.agent?.lastUsage as LiveUsage | null | undefined) ?? liveUsage;
|
|
174
|
+
} catch {}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
setLastLiveUsage(liveUsage);
|
|
178
|
+
} catch {
|
|
179
|
+
// Model, provider, and title stay at event snapshot values; the card renders placeholders
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const telemetry = await collectTurnTelemetry(
|
|
183
|
+
event.turnId,
|
|
184
|
+
event.agent.id,
|
|
185
|
+
event.outcome,
|
|
186
|
+
durationMs,
|
|
187
|
+
{
|
|
188
|
+
cwd: event.agent.cwd,
|
|
189
|
+
provider: agentProvider,
|
|
190
|
+
title: agentTitle,
|
|
191
|
+
model: agentModel,
|
|
192
|
+
timeline: event.timeline,
|
|
193
|
+
gitBefore: gitBefore ?? null,
|
|
194
|
+
liveUsage: getLastLiveUsage(),
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
await context.paseo.agents.ref(event.agent.id).timeline.append({
|
|
199
|
+
type: "plugin",
|
|
200
|
+
id: `top-turn-${event.turnId ?? Date.now()}`,
|
|
201
|
+
kind: TOP_TIMELINE_KIND,
|
|
202
|
+
version: TOP_TIMELINE_VERSION,
|
|
203
|
+
data: telemetry,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
log.info("Appended turn telemetry to timeline", {
|
|
207
|
+
agentId: event.agent.id,
|
|
208
|
+
turnId: event.turnId,
|
|
209
|
+
outcome: event.outcome.kind,
|
|
210
|
+
durationMs,
|
|
211
|
+
cpuPercent: telemetry.cpuPercent,
|
|
212
|
+
memPercent: telemetry.memPercent,
|
|
213
|
+
});
|
|
214
|
+
} catch (err) {
|
|
215
|
+
log.warn("Failed to record turn telemetry", {
|
|
216
|
+
agentId: event.agent.id,
|
|
217
|
+
turnId: event.turnId,
|
|
218
|
+
error: err instanceof Error ? err.message : String(err),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const unsubscribeTurnEnded = server.on("agent.turn_ended", (event, context) => {
|
|
224
|
+
const settingsPromise = handleGetSettings();
|
|
225
|
+
void settingsPromise.then((settings) => {
|
|
226
|
+
if (settings.recordTurnTelemetry === false) {
|
|
227
|
+
turnStartTimes.delete(event.agent.id);
|
|
228
|
+
turnGitBefore.delete(event.agent.id);
|
|
229
|
+
activeTurnIds.delete(event.agent.id);
|
|
230
|
+
lastCanceledAt.delete(event.agent.id);
|
|
231
|
+
lastCanceledUserMessages.delete(event.agent.id);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// On interrupt the daemon emits a second terminal for the turn the cancel
|
|
236
|
+
// already ended. Skip it so the interrupt renders as one card.
|
|
237
|
+
const activeTurnId = activeTurnIds.get(event.agent.id);
|
|
238
|
+
const eventTurnId = event.turnId ?? null;
|
|
239
|
+
const eventUserMessages = countTurns(event.timeline) ?? 0;
|
|
240
|
+
const duplicate =
|
|
241
|
+
isStaleTurnEnd(activeTurnId, eventTurnId) ||
|
|
242
|
+
isInterruptEcho({
|
|
243
|
+
lastCanceledAt: lastCanceledAt.get(event.agent.id) ?? null,
|
|
244
|
+
lastCanceledUserMessages: lastCanceledUserMessages.get(event.agent.id) ?? null,
|
|
245
|
+
eventUserMessages,
|
|
246
|
+
now: Date.now(),
|
|
247
|
+
});
|
|
248
|
+
if (duplicate) {
|
|
249
|
+
log.info("Skipped duplicate turn-end telemetry", {
|
|
250
|
+
agentId: event.agent.id,
|
|
251
|
+
turnId: event.turnId,
|
|
252
|
+
activeTurnId: activeTurnId ?? null,
|
|
253
|
+
});
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
activeTurnIds.delete(event.agent.id);
|
|
257
|
+
if (event.outcome.kind === "canceled") {
|
|
258
|
+
lastCanceledAt.set(event.agent.id, Date.now());
|
|
259
|
+
lastCanceledUserMessages.set(event.agent.id, eventUserMessages);
|
|
260
|
+
} else {
|
|
261
|
+
lastCanceledAt.delete(event.agent.id);
|
|
262
|
+
lastCanceledUserMessages.delete(event.agent.id);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const startTime = turnStartTimes.get(event.agent.id);
|
|
266
|
+
turnStartTimes.delete(event.agent.id);
|
|
267
|
+
const gitBefore = turnGitBefore.get(event.agent.id);
|
|
268
|
+
turnGitBefore.delete(event.agent.id);
|
|
269
|
+
const turnIndex = (turnCounters.get(event.agent.id) ?? 0) + 1;
|
|
270
|
+
turnCounters.set(event.agent.id, turnIndex);
|
|
271
|
+
void appendTurnCard(event, context, startTime, gitBefore, turnIndex);
|
|
272
|
+
}).catch((err) => {
|
|
273
|
+
log.warn("Failed to record turn telemetry", {
|
|
274
|
+
agentId: event.agent.id,
|
|
275
|
+
turnId: event.turnId,
|
|
276
|
+
error: err instanceof Error ? err.message : String(err),
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
return () => {
|
|
282
|
+
activeTurnIds.clear();
|
|
283
|
+
lastCanceledAt.clear();
|
|
284
|
+
lastCanceledUserMessages.clear();
|
|
285
|
+
customPillPoller.stop();
|
|
286
|
+
unsubscribeTurnStarted();
|
|
287
|
+
unsubscribeAgentCreated();
|
|
288
|
+
unsubscribeTurnEnded();
|
|
289
|
+
};
|
|
290
|
+
}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@xpufx/paseo-top",
|
|
3
3
|
"private": false,
|
|
4
4
|
"license": "MIT",
|
|
5
|
-
"version": "0.4.
|
|
5
|
+
"version": "0.4.1",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"stamp": "node -e \"import('./server/vendor/paseo-plugin-helper/version.ts').then(m => m.stampVersion({ targetFile: 'shared/version.ts' }))\"",
|
|
8
8
|
"typecheck": "tsc --noEmit",
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
"paseo-plugin.json",
|
|
22
22
|
"README.md",
|
|
23
23
|
"LICENSE",
|
|
24
|
+
"index.client.tsx",
|
|
25
|
+
"index.server.ts",
|
|
24
26
|
"client",
|
|
25
27
|
"server",
|
|
26
28
|
"shared",
|