@xpufx/paseo-top 0.4.4 → 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 (129) 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 +94 -1
  6. package/client/pill.tsx +690 -614
  7. package/client/resources-query.ts +1 -1
  8. package/client/settings-events.ts +1 -1
  9. package/client/settings-ui.tsx +86 -9
  10. package/client/surface.tsx +308 -276
  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} +9 -3
  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} +51 -18
  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/{surface.tsx → lifecycle/surface.tsx} +6 -6
  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} +50 -58
  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 -234
  48. package/package.json +7 -4
  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 +2 -0
  59. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  60. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  61. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  62. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  63. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  64. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  65. package/server/vendor/permission-audit/index.ts +1 -0
  66. package/server/vendor/permission-audit/server.ts +839 -0
  67. package/shared/helper-version.ts +1 -1
  68. package/shared/multi-host.ts +153 -159
  69. package/shared/resources.ts +145 -0
  70. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  71. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  72. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  73. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  74. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  75. package/shared/vendor/permission-audit/filter.ts +61 -0
  76. package/shared/vendor/permission-audit/index.ts +2 -0
  77. package/shared/vendor/permission-audit/shared.ts +119 -0
  78. package/shared/version.ts +1 -1
  79. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  80. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  81. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  82. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  83. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  84. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  85. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  86. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  87. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  88. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  89. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  90. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  91. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  92. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +0 -446
  93. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +0 -247
  94. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +0 -117
  95. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +0 -53
  96. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +0 -132
  97. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  98. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  99. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +0 -80
  100. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +0 -319
  101. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +0 -150
  102. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +0 -163
  103. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +0 -157
  104. package/client/vendor/paseo-plugin-helper/components/index.ts +0 -26
  105. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  106. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +0 -79
  107. package/client/vendor/paseo-plugin-helper/icon.tsx +0 -39
  108. package/client/vendor/paseo-plugin-helper/index.ts +0 -30
  109. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +0 -49
  110. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +0 -103
  111. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +0 -65
  112. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +0 -368
  113. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +0 -51
  114. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +0 -39
  115. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +0 -39
  116. package/client/vendor/paseo-plugin-helper/layout/index.ts +0 -7
  117. package/client/vendor/paseo-plugin-helper/styles/index.ts +0 -1
  118. package/client/vendor/paseo-plugin-helper/styles/recipes.ts +0 -550
  119. package/client/vendor/paseo-plugin-helper/theme/flair.ts +0 -76
  120. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  121. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  122. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  123. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  124. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  125. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  126. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  127. /package/client/vendor/paseo-plugin-helper/{command-center.ts → core/command-center.ts} +0 -0
  128. /package/client/vendor/paseo-plugin-helper/{snapshot.ts → core/snapshot.ts} +0 -0
  129. /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,6 +1,11 @@
1
1
  import type { PluginServerContext } from "@getpaseo/plugin/server";
2
- import { guardRpcHandler } from "./server/vendor/paseo-plugin-helper/index";
3
- import { registerPermissionAuditServer } from "permission-audit/server";
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";
4
9
  import {
5
10
  getSystemResourcesRpc,
6
11
  getCustomPillsRpc,
@@ -32,266 +37,283 @@ import {
32
37
  } from "./server/resources";
33
38
  import { resolveTimelineCadence, shouldAppendTimelineForTurn } from "./shared/resources";
34
39
 
35
- export default function contribute(server: PluginServerContext) {
36
- void customPillPoller.start();
37
-
38
- // Shed load instead of hanging the daemon RPC: saturated or slow handlers
39
- // answer from the last good snapshot (system-resources) or fail fast.
40
- // WARNs are rate-limited (one per minute per cause): saturation fires once
41
- // per poll tick per caller while the modal is open, which flooded the log.
42
- let lastSystemResources: SystemResources | null = null;
43
- const lastWarnAt = { timeout: 0, saturated: 0 };
44
- const WARN_COOLDOWN_MS = 60_000;
45
- const guardedSystemResources = guardRpcHandler(
46
- async (input: Parameters<typeof handleGetSystemResources>[0]) => {
47
- const resources = await handleGetSystemResources(input);
48
- lastSystemResources = resources;
49
- 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 });
50
58
  },
51
- {
52
- timeoutMs: 5000,
53
- maxInflight: 4,
54
- getStale: () => lastSystemResources,
55
- onTimeout: ({ timeoutMs }) => {
56
- const now = Date.now();
57
- if (now - lastWarnAt.timeout < WARN_COOLDOWN_MS) return;
58
- lastWarnAt.timeout = now;
59
- log.warn("system-resources handler timed out", { timeoutMs });
60
- },
61
- onSaturated: ({ maxInflight }) => {
62
- const now = Date.now();
63
- if (now - lastWarnAt.saturated < WARN_COOLDOWN_MS) return;
64
- lastWarnAt.saturated = now;
65
- log.warn("system-resources handler saturated, serving stale", { maxInflight });
66
- },
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 });
67
64
  },
68
- );
69
-
70
- server.handle(topSettingsContract.get, handleGetSettings);
71
- server.handle(topSettingsContract.update, handleUpdateSettings);
72
- server.handle(topSettingsContract.reset, handleResetSettings);
73
- server.handle(getSystemResourcesRpc, guardedSystemResources);
74
- server.handle(getCustomPillsRpc, handleGetCustomPills);
75
- server.handle(listCustomPillsRpc, handleListCustomPills);
76
- server.handle(runCustomPillModalCommandRpc, handleRunCustomPillModalCommand);
65
+ },
66
+ );
77
67
 
78
- const { unsubscribe: unsubscribePermissionAudit } = registerPermissionAuditServer(
79
- server as unknown as Parameters<typeof registerPermissionAuditServer>[0],
80
- { logger: log },
81
- );
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
+ });
82
82
 
83
- const turnStartTimes = new Map<string, number>();
84
- const turnGitBefore = new Map<string, { insertions: number; deletions: number; filesChanged: number }>();
85
- // Live turn per agent, set on turn_started and consumed by its terminal, used
86
- // to drop terminals that cannot belong to it (see isStaleTurnEnd).
87
- const activeTurnIds = new Map<string, string | null>();
88
- // Time and timeline user_message count of the last canceled terminal, used to
89
- // drop the daemon's extra interrupt terminal (see isInterruptEcho).
90
- const lastCanceledAt = new Map<string, number>();
91
- const lastCanceledUserMessages = new Map<string, number>();
92
- // Per-agent turn counter for the timeline cadence option (0 = never,
93
- // 1 = every turn, N>1 = every Nth turn). Counts deduped turn_ended events.
94
- const turnCounters = new Map<string, number>();
95
-
96
- const unsubscribeTurnStarted = server.on("agent.turn_started", (event, context) => {
97
- turnStartTimes.set(event.agent.id, Date.now());
98
- activeTurnIds.set(event.agent.id, event.turnId ?? null);
99
- if ((event.agent as any)?.lastUsage) {
100
- setLastLiveUsage((event.agent as any).lastUsage);
101
- }
102
- void context.paseo.agents
103
- .ref(event.agent.id)
104
- .refresh()
105
- .then((refetched) => {
106
- if (refetched && (refetched.agent as any)?.lastUsage) {
107
- setLastLiveUsage((refetched.agent as any).lastUsage);
108
- }
109
- })
110
- .catch(() => {});
111
- void collectGitDiffStat(event.agent.cwd).then(
112
- (before) => {
113
- if (before) {
114
- turnGitBefore.set(event.agent.id, before);
115
- } else {
116
- turnGitBefore.delete(event.agent.id);
117
- }
118
- },
119
- () => {
120
- turnGitBefore.delete(event.agent.id);
121
- },
122
- );
123
- });
83
+ const permissionAuditModule = createPermissionAuditFeature<PluginServerContext>({
84
+ logger: log,
85
+ });
124
86
 
125
- const unsubscribeAgentCreated = server.on("agent.created", (event) => {
126
- if ((event.agent as any)?.lastUsage) {
127
- 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;
128
99
  }
129
- });
100
+ const durationMs = startTime ? Date.now() - startTime : undefined;
130
101
 
131
- async function appendTurnCard(
132
- event: any,
133
- context: any,
134
- startTime: number | undefined,
135
- gitBefore: { insertions: number; deletions: number; filesChanged: number } | undefined,
136
- turnIndex: number,
137
- ): 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;
138
105
  try {
139
- const settings = await handleGetSettings();
140
- const cadence = resolveTimelineCadence(settings);
141
- if (!shouldAppendTimelineForTurn(cadence, turnIndex)) {
142
- return;
143
- }
144
- const durationMs = startTime ? Date.now() - startTime : undefined;
145
-
146
- let agentModel: string | null = null;
147
- let agentProvider: string | null = event.agent.provider ?? null;
148
- let agentTitle: string | null = event.agent.title ?? null;
149
- try {
150
- const refetched = await context.paseo.agents.ref(event.agent.id).refresh();
151
- agentModel = refetched?.agent?.model ?? agentModel;
152
- agentProvider = refetched?.agent?.provider ?? agentProvider;
153
- agentTitle = refetched?.agent?.title ?? agentTitle;
154
- let liveUsage =
155
- (refetched?.agent?.lastUsage as LiveUsage | null | undefined) ??
156
- ((event.agent as any)?.lastUsage as LiveUsage | null | undefined) ??
157
- null;
158
- 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 =
159
127
  liveUsage?.inputTokens == null &&
160
128
  liveUsage?.outputTokens == null &&
161
129
  (liveUsage as any)?.contextWindowUsedTokens == null;
162
- if (lacksTokens) {
163
- const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
164
- await sleep(150);
130
+ if (stillLacks) {
131
+ await sleep(250);
165
132
  try {
166
- const retry1 = await context.paseo.agents.ref(event.agent.id).refresh();
133
+ const retry2 = await context.paseo.agents.ref(event.agent.id).refresh();
167
134
  liveUsage =
168
- (retry1?.agent?.lastUsage as LiveUsage | null | undefined) ?? liveUsage;
135
+ (retry2?.agent?.lastUsage as LiveUsage | null | undefined) ?? liveUsage;
169
136
  } catch {}
170
- const stillLacks =
171
- liveUsage?.inputTokens == null &&
172
- liveUsage?.outputTokens == null &&
173
- (liveUsage as any)?.contextWindowUsedTokens == null;
174
- if (stillLacks) {
175
- await sleep(250);
176
- try {
177
- const retry2 = await context.paseo.agents.ref(event.agent.id).refresh();
178
- liveUsage =
179
- (retry2?.agent?.lastUsage as LiveUsage | null | undefined) ?? liveUsage;
180
- } catch {}
181
- }
182
137
  }
183
- setLastLiveUsage(liveUsage);
184
- } catch {
185
- // Model, provider, and title stay at event snapshot values; the card renders placeholders
186
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
+ });
167
+
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>();
187
194
 
188
- const telemetry = await collectTurnTelemetry(
189
- event.turnId,
190
- event.agent.id,
191
- event.outcome,
192
- durationMs,
193
- {
194
- cwd: event.agent.cwd,
195
- provider: agentProvider,
196
- title: agentTitle,
197
- model: agentModel,
198
- timeline: event.timeline,
199
- gitBefore: gitBefore ?? null,
200
- liveUsage: getLastLiveUsage(),
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);
201
220
  },
202
221
  );
222
+ });
203
223
 
204
- await context.paseo.agents.ref(event.agent.id).timeline.append({
205
- type: "plugin",
206
- id: `top-turn-${event.turnId ?? Date.now()}`,
207
- kind: TOP_TIMELINE_KIND,
208
- version: TOP_TIMELINE_VERSION,
209
- data: telemetry,
210
- });
224
+ const unsubscribeAgentCreated = server.on("agent.created", (event) => {
225
+ if ((event.agent as any)?.lastUsage) {
226
+ setLastLiveUsage((event.agent as any).lastUsage);
227
+ }
228
+ });
211
229
 
212
- log.info("Appended turn telemetry to timeline", {
213
- agentId: event.agent.id,
214
- turnId: event.turnId,
215
- outcome: event.outcome.kind,
216
- durationMs,
217
- cpuPercent: telemetry.cpuPercent,
218
- memPercent: telemetry.memPercent,
219
- });
220
- } catch (err) {
221
- log.warn("Failed to record turn telemetry", {
222
- agentId: event.agent.id,
223
- turnId: event.turnId,
224
- error: err instanceof Error ? err.message : String(err),
225
- });
226
- }
227
- }
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
+ }
228
241
 
229
- const unsubscribeTurnEnded = server.on("agent.turn_ended", (event, context) => {
230
- const settingsPromise = handleGetSettings();
231
- void settingsPromise.then((settings) => {
232
- if (settings.recordTurnTelemetry === false) {
233
- turnStartTimes.delete(event.agent.id);
234
- 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
+ }
235
261
  activeTurnIds.delete(event.agent.id);
236
- lastCanceledAt.delete(event.agent.id);
237
- lastCanceledUserMessages.delete(event.agent.id);
238
- return;
239
- }
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
+ }
240
269
 
241
- // On interrupt the daemon emits a second terminal for the turn the cancel
242
- // already ended. Skip it so the interrupt renders as one card.
243
- const activeTurnId = activeTurnIds.get(event.agent.id);
244
- const eventTurnId = event.turnId ?? null;
245
- const eventUserMessages = countTurns(event.timeline) ?? 0;
246
- const duplicate =
247
- isStaleTurnEnd(activeTurnId, eventTurnId) ||
248
- isInterruptEcho({
249
- lastCanceledAt: lastCanceledAt.get(event.agent.id) ?? null,
250
- lastCanceledUserMessages: lastCanceledUserMessages.get(event.agent.id) ?? null,
251
- eventUserMessages,
252
- now: Date.now(),
253
- });
254
- if (duplicate) {
255
- 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", {
256
279
  agentId: event.agent.id,
257
280
  turnId: event.turnId,
258
- activeTurnId: activeTurnId ?? null,
281
+ error: err instanceof Error ? err.message : String(err),
259
282
  });
260
- return;
261
- }
262
- activeTurnIds.delete(event.agent.id);
263
- if (event.outcome.kind === "canceled") {
264
- lastCanceledAt.set(event.agent.id, Date.now());
265
- lastCanceledUserMessages.set(event.agent.id, eventUserMessages);
266
- } else {
267
- lastCanceledAt.delete(event.agent.id);
268
- lastCanceledUserMessages.delete(event.agent.id);
269
- }
270
-
271
- const startTime = turnStartTimes.get(event.agent.id);
272
- turnStartTimes.delete(event.agent.id);
273
- const gitBefore = turnGitBefore.get(event.agent.id);
274
- turnGitBefore.delete(event.agent.id);
275
- const turnIndex = (turnCounters.get(event.agent.id) ?? 0) + 1;
276
- turnCounters.set(event.agent.id, turnIndex);
277
- void appendTurnCard(event, context, startTime, gitBefore, turnIndex);
278
- }).catch((err) => {
279
- log.warn("Failed to record turn telemetry", {
280
- agentId: event.agent.id,
281
- turnId: event.turnId,
282
- error: err instanceof Error ? err.message : String(err),
283
283
  });
284
284
  });
285
- });
286
285
 
287
- return () => {
288
- activeTurnIds.clear();
289
- lastCanceledAt.clear();
290
- lastCanceledUserMessages.clear();
291
- customPillPoller.stop();
292
- unsubscribePermissionAudit();
293
- unsubscribeTurnStarted();
294
- unsubscribeAgentCreated();
295
- unsubscribeTurnEnded();
296
- };
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);
297
319
  }