@xpufx/paseo-top 0.4.3 → 0.4.5

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.
Files changed (130) hide show
  1. package/OVERVIEW.md +17 -0
  2. package/client/host-ui.tsx +184 -0
  3. package/client/multi-host-view.tsx +57 -43
  4. package/client/multi-host.ts +36 -13
  5. package/client/pill-labels.ts +124 -7
  6. package/client/pill.tsx +724 -669
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +33 -0
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +310 -274
  11. package/client/telemetry-copy.ts +8 -2
  12. package/client/telemetry.tsx +372 -321
  13. package/client/turn-panel.tsx +126 -104
  14. package/client/vendor/paseo-plugin-helper/{host.ts → core/host.ts} +21 -61
  15. package/client/vendor/paseo-plugin-helper/core/index.ts +76 -0
  16. package/client/vendor/paseo-plugin-helper/core/plugin-load-count.ts +89 -0
  17. package/client/vendor/paseo-plugin-helper/{query-refresh.ts → core/query-refresh.ts} +2 -2
  18. package/client/vendor/paseo-plugin-helper/{query.ts → core/query.ts} +2 -2
  19. package/client/vendor/paseo-plugin-helper/{settings.ts → core/settings.ts} +1 -1
  20. package/client/vendor/paseo-plugin-helper/{shared-settings.ts → core/shared-settings.ts} +2 -2
  21. package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/clipboard.ts +2 -2
  22. package/client/vendor/paseo-plugin-helper/lifecycle/flair.ts +26 -0
  23. package/client/vendor/paseo-plugin-helper/{theme/color-utils.ts → lifecycle/host-color.ts} +95 -38
  24. package/client/vendor/paseo-plugin-helper/lifecycle/host-theme.tsx +109 -0
  25. package/client/vendor/paseo-plugin-helper/lifecycle/index.ts +23 -0
  26. package/client/vendor/paseo-plugin-helper/lifecycle/multi-host.ts +66 -0
  27. package/client/vendor/paseo-plugin-helper/{panel.tsx → lifecycle/panel.tsx} +7 -7
  28. package/client/vendor/paseo-plugin-helper/{pill.tsx → lifecycle/pill.tsx} +118 -338
  29. package/client/vendor/paseo-plugin-helper/lifecycle/providers.tsx +46 -0
  30. package/client/vendor/paseo-plugin-helper/lifecycle/scroll-owner.ts +26 -0
  31. package/client/vendor/paseo-plugin-helper/lifecycle/surface.tsx +140 -0
  32. package/client/vendor/paseo-plugin-helper/ui/color.ts +1 -0
  33. package/client/vendor/paseo-plugin-helper/ui/content.tsx +1326 -0
  34. package/client/vendor/paseo-plugin-helper/ui/controls.tsx +1018 -0
  35. package/client/vendor/paseo-plugin-helper/{components/DataTable.tsx → ui/data.tsx} +55 -47
  36. package/client/vendor/paseo-plugin-helper/ui/index.ts +44 -0
  37. package/client/vendor/paseo-plugin-helper/ui/layout.tsx +263 -0
  38. package/client/vendor/paseo-plugin-helper/ui/modal.tsx +111 -0
  39. package/client/vendor/paseo-plugin-helper/ui/plugin-reload-banner.tsx +119 -0
  40. package/client/vendor/paseo-plugin-helper/{settings-screen.tsx → ui/settings-screen.tsx} +3 -3
  41. package/client/vendor/paseo-plugin-helper/ui/settings.ts +36 -0
  42. package/client/vendor/paseo-plugin-helper/ui/text.tsx +821 -0
  43. package/client/vendor/paseo-plugin-helper/ui/theme.tsx +1 -0
  44. package/client/vendor/permission-audit/client.tsx +706 -0
  45. package/client/vendor/permission-audit/index.tsx +1 -0
  46. package/index.client.tsx +58 -37
  47. package/index.server.ts +256 -227
  48. package/package.json +14 -2
  49. package/paseo-plugin.json +2 -0
  50. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  51. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  52. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  53. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  54. package/screenshots/paseo-top-dashboard.jpg +0 -0
  55. package/screenshots/paseo-top-general-view.jpg +0 -0
  56. package/server/resources.ts +98 -0
  57. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  58. package/server/vendor/paseo-plugin-helper/index.ts +4 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
  61. package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
  62. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  63. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  64. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  65. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  66. package/server/vendor/paseo-plugin-helper/tickets.ts +1232 -0
  67. package/server/vendor/permission-audit/index.ts +1 -0
  68. package/server/vendor/permission-audit/server.ts +839 -0
  69. package/shared/helper-version.ts +20 -0
  70. package/shared/multi-host.ts +156 -155
  71. package/shared/resources.ts +146 -1
  72. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  73. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  74. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  75. package/shared/vendor/paseo-plugin-helper/index.ts +2 -0
  76. package/shared/vendor/paseo-plugin-helper/tickets.ts +1104 -0
  77. package/shared/vendor/permission-audit/filter.ts +61 -0
  78. package/shared/vendor/permission-audit/index.ts +2 -0
  79. package/shared/vendor/permission-audit/shared.ts +119 -0
  80. package/shared/version.ts +1 -1
  81. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  82. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  83. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -156
  84. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -184
  85. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -225
  86. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  87. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  88. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  89. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  90. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  91. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  92. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -75
  93. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  94. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  95. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  96. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  97. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -118
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -80
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -215
  100. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  101. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  102. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  103. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  104. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  105. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -25
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -224
  107. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  108. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  109. package/client/vendor/paseo-plugin-helper/index.ts +0 -29
  110. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  111. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  112. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  113. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -378
  114. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  115. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  117. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  118. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  119. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  120. package/client/vendor/paseo-plugin-helper/surface.tsx +0 -80
  121. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  122. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  123. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  124. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  125. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  126. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  127. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  128. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  129. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  130. /package/client/vendor/paseo-plugin-helper/{utils → lifecycle}/haptics.ts +0 -0
package/index.client.tsx CHANGED
@@ -9,8 +9,9 @@ import {
9
9
  TextInput as HostTextInput,
10
10
  copyText,
11
11
  } from "@getpaseo/plugin/client/react-native";
12
- import { initClientHelpers } from "./client/vendor/paseo-plugin-helper/index";
13
- import { contributeClient } from "./client/pill";
12
+ import { initClientHelpers } from "./client/vendor/paseo-plugin-helper/core/index";
13
+ import { composeFeatureModules } from "./shared/vendor/paseo-plugin-helper/index";
14
+ import { contributeClient as contributePillsClient } from "./client/pill";
14
15
  import { TopTimelineTelemetryCard } from "./client/telemetry";
15
16
  import { TurnCounterPanel } from "./client/turn-panel";
16
17
  import {
@@ -30,42 +31,62 @@ initClientHelpers({
30
31
  TextInput: HostTextInput,
31
32
  });
32
33
 
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
- });
34
+ const topTimelineTelemetryFeature = {
35
+ id: "top-timeline-telemetry",
36
+ contributeClient(client: PluginClientContext) {
37
+ return client.addTimelineRenderer({
38
+ kind: TOP_TIMELINE_KIND,
39
+ version: TOP_TIMELINE_VERSION,
40
+ schema: topTimelineTelemetrySchema,
41
+ Component: TopTimelineTelemetryCard,
42
+ });
43
+ },
44
+ };
45
+
46
+ const topCustomPillsFeature = {
47
+ id: "top-custom-pills",
48
+ contributeClient(client: PluginClientContext) {
49
+ return contributePillsClient(client);
50
+ },
51
+ };
40
52
 
41
- const cleanupClient = contributeClient(client);
53
+ const topTurnCounterFeature = {
54
+ id: "top-turn-counter",
55
+ contributeClient(client: PluginClientContext) {
56
+ // Agent-scoped panel: exposes the deterministic turn index for the active
57
+ // conversation (Turn #N + canonical seq) so a human and the agent resolve the
58
+ // same reference. See client/turn-counter.ts. Agent panels are opened through
59
+ // a command-center item, which forwards the active agent's openPanel.
60
+ const removeTurnPanel = client.addWorkspacePanel({
61
+ id: "top-turn-counter",
62
+ title: "Turn Counter",
63
+ icon: "Repeat",
64
+ context: "agent",
65
+ Component: TurnCounterPanel,
66
+ });
67
+ const removeTurnCommand = client.addCommandCenterItem({
68
+ id: "top-turn-counter",
69
+ title: "Turn Counter",
70
+ icon: "Repeat",
71
+ context: "agent",
72
+ keywords: ["turn", "seq", "counter", "conversation"],
73
+ onSelect(ctx) {
74
+ ctx.openPanel("top-turn-counter");
75
+ },
76
+ });
77
+ return () => {
78
+ removeTurnCommand();
79
+ removeTurnPanel();
80
+ };
81
+ },
82
+ };
42
83
 
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
- });
84
+ const topClientFeatures = composeFeatureModules<any, PluginClientContext>([
85
+ topTimelineTelemetryFeature,
86
+ topCustomPillsFeature,
87
+ topTurnCounterFeature,
88
+ ]);
64
89
 
65
- return () => {
66
- removeTurnCommand();
67
- removeTurnPanel();
68
- removeTimelineRenderer();
69
- cleanupClient();
70
- };
90
+ export default function contribute(client: PluginClientContext) {
91
+ return topClientFeatures.contributeClient(client);
71
92
  }
package/index.server.ts CHANGED
@@ -1,5 +1,11 @@
1
1
  import type { PluginServerContext } from "@getpaseo/plugin/server";
2
- import { guardRpcHandler } from "./server/vendor/paseo-plugin-helper/index";
2
+ import {
3
+ guardRpcHandler,
4
+ createCustomPillsFeature,
5
+ createTimelineTelemetryFeature,
6
+ } from "./server/vendor/paseo-plugin-helper/index";
7
+ import { composeFeatureModules } from "./shared/vendor/paseo-plugin-helper/index";
8
+ import { createPermissionAuditFeature } from "./server/vendor/permission-audit";
3
9
  import {
4
10
  getSystemResourcesRpc,
5
11
  getCustomPillsRpc,
@@ -31,260 +37,283 @@ import {
31
37
  } from "./server/resources";
32
38
  import { resolveTimelineCadence, shouldAppendTimelineForTurn } from "./shared/resources";
33
39
 
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;
40
+ let lastSystemResources: SystemResources | null = null;
41
+ const lastWarnAt = { timeout: 0, saturated: 0 };
42
+ const WARN_COOLDOWN_MS = 60_000;
43
+ const guardedSystemResources = guardRpcHandler(
44
+ async (input: Parameters<typeof handleGetSystemResources>[0]) => {
45
+ const resources = await handleGetSystemResources(input);
46
+ lastSystemResources = resources;
47
+ return resources;
48
+ },
49
+ {
50
+ timeoutMs: 5000,
51
+ maxInflight: 4,
52
+ getStale: () => lastSystemResources,
53
+ onTimeout: ({ timeoutMs }) => {
54
+ const now = Date.now();
55
+ if (now - lastWarnAt.timeout < WARN_COOLDOWN_MS) return;
56
+ lastWarnAt.timeout = now;
57
+ log.warn("system-resources handler timed out", { timeoutMs });
49
58
  },
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
- },
59
+ onSaturated: ({ maxInflight }) => {
60
+ const now = Date.now();
61
+ if (now - lastWarnAt.saturated < WARN_COOLDOWN_MS) return;
62
+ lastWarnAt.saturated = now;
63
+ log.warn("system-resources handler saturated, serving stale", { maxInflight });
66
64
  },
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);
65
+ },
66
+ );
76
67
 
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>();
68
+ const customPillsModule = createCustomPillsFeature<PluginServerContext>({
69
+ id: "top-custom-pills",
70
+ poller: customPillPoller,
71
+ contracts: {
72
+ get: getCustomPillsRpc,
73
+ list: listCustomPillsRpc,
74
+ run: runCustomPillModalCommandRpc,
75
+ },
76
+ handlers: {
77
+ get: handleGetCustomPills,
78
+ list: handleListCustomPills,
79
+ run: handleRunCustomPillModalCommand,
80
+ },
81
+ });
89
82
 
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
- });
83
+ const permissionAuditModule = createPermissionAuditFeature<PluginServerContext>({
84
+ logger: log,
85
+ });
118
86
 
119
- const unsubscribeAgentCreated = server.on("agent.created", (event) => {
120
- if ((event.agent as any)?.lastUsage) {
121
- setLastLiveUsage((event.agent as any).lastUsage);
87
+ async function appendTurnCard(
88
+ event: any,
89
+ context: any,
90
+ startTime: number | undefined,
91
+ gitBefore: { insertions: number; deletions: number; filesChanged: number } | undefined,
92
+ turnIndex: number,
93
+ ): Promise<void> {
94
+ try {
95
+ const settings = await handleGetSettings();
96
+ const cadence = resolveTimelineCadence(settings);
97
+ if (!shouldAppendTimelineForTurn(cadence, turnIndex)) {
98
+ return;
122
99
  }
123
- });
100
+ const durationMs = startTime ? Date.now() - startTime : undefined;
124
101
 
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> {
102
+ let agentModel: string | null = null;
103
+ let agentProvider: string | null = event.agent.provider ?? null;
104
+ let agentTitle: string | null = event.agent.title ?? null;
132
105
  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 =
106
+ const refetched = await context.paseo.agents.ref(event.agent.id).refresh();
107
+ agentModel = refetched?.agent?.model ?? agentModel;
108
+ agentProvider = refetched?.agent?.provider ?? agentProvider;
109
+ agentTitle = refetched?.agent?.title ?? agentTitle;
110
+ let liveUsage =
111
+ (refetched?.agent?.lastUsage as LiveUsage | null | undefined) ??
112
+ ((event.agent as any)?.lastUsage as LiveUsage | null | undefined) ??
113
+ null;
114
+ const lacksTokens =
115
+ liveUsage?.inputTokens == null &&
116
+ liveUsage?.outputTokens == null &&
117
+ (liveUsage as any)?.contextWindowUsedTokens == null;
118
+ if (lacksTokens) {
119
+ const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
120
+ await sleep(150);
121
+ try {
122
+ const retry1 = await context.paseo.agents.ref(event.agent.id).refresh();
123
+ liveUsage =
124
+ (retry1?.agent?.lastUsage as LiveUsage | null | undefined) ?? liveUsage;
125
+ } catch {}
126
+ const stillLacks =
153
127
  liveUsage?.inputTokens == null &&
154
128
  liveUsage?.outputTokens == null &&
155
129
  (liveUsage as any)?.contextWindowUsedTokens == null;
156
- if (lacksTokens) {
157
- const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
158
- await sleep(150);
130
+ if (stillLacks) {
131
+ await sleep(250);
159
132
  try {
160
- const retry1 = await context.paseo.agents.ref(event.agent.id).refresh();
133
+ const retry2 = await context.paseo.agents.ref(event.agent.id).refresh();
161
134
  liveUsage =
162
- (retry1?.agent?.lastUsage as LiveUsage | null | undefined) ?? liveUsage;
135
+ (retry2?.agent?.lastUsage as LiveUsage | null | undefined) ?? liveUsage;
163
136
  } 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
137
  }
177
- setLastLiveUsage(liveUsage);
178
- } catch {
179
- // Model, provider, and title stay at event snapshot values; the card renders placeholders
180
138
  }
139
+ setLastLiveUsage(liveUsage);
140
+ } catch {
141
+ // Model, provider, and title stay at event snapshot values; the card renders placeholders
142
+ }
143
+
144
+ const telemetry = await collectTurnTelemetry(
145
+ event.turnId,
146
+ event.agent.id,
147
+ event.outcome,
148
+ durationMs,
149
+ {
150
+ cwd: event.agent.cwd,
151
+ provider: agentProvider,
152
+ title: agentTitle,
153
+ model: agentModel,
154
+ timeline: event.timeline,
155
+ gitBefore: gitBefore ?? null,
156
+ liveUsage: getLastLiveUsage(),
157
+ },
158
+ );
159
+
160
+ await context.paseo.agents.ref(event.agent.id).timeline.append({
161
+ type: "plugin",
162
+ id: `top-turn-${event.turnId ?? Date.now()}`,
163
+ kind: TOP_TIMELINE_KIND,
164
+ version: TOP_TIMELINE_VERSION,
165
+ data: telemetry,
166
+ });
181
167
 
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(),
168
+ log.info("Appended turn telemetry to timeline", {
169
+ agentId: event.agent.id,
170
+ turnId: event.turnId,
171
+ outcome: event.outcome.kind,
172
+ durationMs,
173
+ cpuPercent: telemetry.cpuPercent,
174
+ memPercent: telemetry.memPercent,
175
+ });
176
+ } catch (err) {
177
+ log.warn("Failed to record turn telemetry", {
178
+ agentId: event.agent.id,
179
+ turnId: event.turnId,
180
+ error: err instanceof Error ? err.message : String(err),
181
+ });
182
+ }
183
+ }
184
+
185
+ const timelineTelemetryModule = createTimelineTelemetryFeature<PluginServerContext>({
186
+ id: "top-timeline-telemetry",
187
+ serverTap: (server) => {
188
+ const turnStartTimes = new Map<string, number>();
189
+ const turnGitBefore = new Map<string, { insertions: number; deletions: number; filesChanged: number }>();
190
+ const activeTurnIds = new Map<string, string | null>();
191
+ const lastCanceledAt = new Map<string, number>();
192
+ const lastCanceledUserMessages = new Map<string, number>();
193
+ const turnCounters = new Map<string, number>();
194
+
195
+ const unsubscribeTurnStarted = server.on("agent.turn_started", (event, context) => {
196
+ turnStartTimes.set(event.agent.id, Date.now());
197
+ activeTurnIds.set(event.agent.id, event.turnId ?? null);
198
+ if ((event.agent as any)?.lastUsage) {
199
+ setLastLiveUsage((event.agent as any).lastUsage);
200
+ }
201
+ void context.paseo.agents
202
+ .ref(event.agent.id)
203
+ .refresh()
204
+ .then((refetched) => {
205
+ if (refetched && (refetched.agent as any)?.lastUsage) {
206
+ setLastLiveUsage((refetched.agent as any).lastUsage);
207
+ }
208
+ })
209
+ .catch(() => {});
210
+ void collectGitDiffStat(event.agent.cwd).then(
211
+ (before) => {
212
+ if (before) {
213
+ turnGitBefore.set(event.agent.id, before);
214
+ } else {
215
+ turnGitBefore.delete(event.agent.id);
216
+ }
217
+ },
218
+ () => {
219
+ turnGitBefore.delete(event.agent.id);
195
220
  },
196
221
  );
222
+ });
197
223
 
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
- });
224
+ const unsubscribeAgentCreated = server.on("agent.created", (event) => {
225
+ if ((event.agent as any)?.lastUsage) {
226
+ setLastLiveUsage((event.agent as any).lastUsage);
227
+ }
228
+ });
205
229
 
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
- }
230
+ const unsubscribeTurnEnded = server.on("agent.turn_ended", (event, context) => {
231
+ const settingsPromise = handleGetSettings();
232
+ void settingsPromise.then((settings) => {
233
+ if (settings.recordTurnTelemetry === false) {
234
+ turnStartTimes.delete(event.agent.id);
235
+ turnGitBefore.delete(event.agent.id);
236
+ activeTurnIds.delete(event.agent.id);
237
+ lastCanceledAt.delete(event.agent.id);
238
+ lastCanceledUserMessages.delete(event.agent.id);
239
+ return;
240
+ }
222
241
 
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);
242
+ const activeTurnId = activeTurnIds.get(event.agent.id);
243
+ const eventTurnId = event.turnId ?? null;
244
+ const eventUserMessages = countTurns(event.timeline) ?? 0;
245
+ const duplicate =
246
+ isStaleTurnEnd(activeTurnId, eventTurnId) ||
247
+ isInterruptEcho({
248
+ lastCanceledAt: lastCanceledAt.get(event.agent.id) ?? null,
249
+ lastCanceledUserMessages: lastCanceledUserMessages.get(event.agent.id) ?? null,
250
+ eventUserMessages,
251
+ now: Date.now(),
252
+ });
253
+ if (duplicate) {
254
+ log.info("Skipped duplicate turn-end telemetry", {
255
+ agentId: event.agent.id,
256
+ turnId: event.turnId,
257
+ activeTurnId: activeTurnId ?? null,
258
+ });
259
+ return;
260
+ }
229
261
  activeTurnIds.delete(event.agent.id);
230
- lastCanceledAt.delete(event.agent.id);
231
- lastCanceledUserMessages.delete(event.agent.id);
232
- return;
233
- }
262
+ if (event.outcome.kind === "canceled") {
263
+ lastCanceledAt.set(event.agent.id, Date.now());
264
+ lastCanceledUserMessages.set(event.agent.id, eventUserMessages);
265
+ } else {
266
+ lastCanceledAt.delete(event.agent.id);
267
+ lastCanceledUserMessages.delete(event.agent.id);
268
+ }
234
269
 
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", {
270
+ const startTime = turnStartTimes.get(event.agent.id);
271
+ turnStartTimes.delete(event.agent.id);
272
+ const gitBefore = turnGitBefore.get(event.agent.id);
273
+ turnGitBefore.delete(event.agent.id);
274
+ const turnIndex = (turnCounters.get(event.agent.id) ?? 0) + 1;
275
+ turnCounters.set(event.agent.id, turnIndex);
276
+ void appendTurnCard(event, context, startTime, gitBefore, turnIndex);
277
+ }).catch((err) => {
278
+ log.warn("Failed to record turn telemetry", {
250
279
  agentId: event.agent.id,
251
280
  turnId: event.turnId,
252
- activeTurnId: activeTurnId ?? null,
281
+ error: err instanceof Error ? err.message : String(err),
253
282
  });
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
283
  });
278
284
  });
279
- });
280
285
 
281
- return () => {
282
- activeTurnIds.clear();
283
- lastCanceledAt.clear();
284
- lastCanceledUserMessages.clear();
285
- customPillPoller.stop();
286
- unsubscribeTurnStarted();
287
- unsubscribeAgentCreated();
288
- unsubscribeTurnEnded();
289
- };
286
+ return () => {
287
+ activeTurnIds.clear();
288
+ lastCanceledAt.clear();
289
+ lastCanceledUserMessages.clear();
290
+ turnStartTimes.clear();
291
+ turnGitBefore.clear();
292
+ turnCounters.clear();
293
+ unsubscribeTurnStarted();
294
+ unsubscribeAgentCreated();
295
+ unsubscribeTurnEnded();
296
+ };
297
+ },
298
+ });
299
+
300
+ const topCoreModule = {
301
+ id: "top-core",
302
+ contributeServer(server: PluginServerContext) {
303
+ server.handle(topSettingsContract.get, handleGetSettings);
304
+ server.handle(topSettingsContract.update, handleUpdateSettings);
305
+ server.handle(topSettingsContract.reset, handleResetSettings);
306
+ server.handle(getSystemResourcesRpc, guardedSystemResources);
307
+ },
308
+ };
309
+
310
+ const topFeatures = composeFeatureModules<PluginServerContext>([
311
+ customPillsModule,
312
+ permissionAuditModule,
313
+ timelineTelemetryModule,
314
+ topCoreModule,
315
+ ]);
316
+
317
+ export default function contribute(server: PluginServerContext) {
318
+ return topFeatures.contributeServer(server);
290
319
  }