@xpufx/paseo-top 0.4.4 → 0.4.7

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 +18 -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 +692 -616
  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/npm-shrinkwrap.json +44 -0
  49. package/package.json +8 -4
  50. package/paseo-plugin.json +2 -0
  51. package/screenshots/1-paseo-top-modal-system.jpg +0 -0
  52. package/screenshots/2-paseo-top-modal-workspace.jpg +0 -0
  53. package/screenshots/3-paseo-top-modal-settings.jpg +0 -0
  54. package/screenshots/4-paseo-top-modal-about.jpg +0 -0
  55. package/screenshots/paseo-top-dashboard.jpg +0 -0
  56. package/screenshots/paseo-top-general-view.jpg +0 -0
  57. package/server/resources.ts +98 -0
  58. package/server/vendor/paseo-plugin-helper/custom-pills.ts +70 -0
  59. package/server/vendor/paseo-plugin-helper/index.ts +2 -0
  60. package/server/vendor/paseo-plugin-helper/logger.ts +5 -16
  61. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +2 -2
  62. package/server/vendor/paseo-plugin-helper/storage-guard.ts +294 -0
  63. package/server/vendor/paseo-plugin-helper/storage.ts +36 -2
  64. package/server/vendor/paseo-plugin-helper/telemetry.ts +486 -0
  65. package/server/vendor/paseo-plugin-helper/tickets.ts +18 -67
  66. package/server/vendor/permission-audit/index.ts +1 -0
  67. package/server/vendor/permission-audit/server.ts +839 -0
  68. package/shared/helper-version.ts +1 -1
  69. package/shared/multi-host.ts +153 -159
  70. package/shared/resources.ts +145 -0
  71. package/shared/vendor/paseo-plugin-helper/async.ts +14 -7
  72. package/shared/vendor/paseo-plugin-helper/features.ts +244 -0
  73. package/shared/vendor/paseo-plugin-helper/forge.ts +27 -0
  74. package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
  75. package/shared/vendor/paseo-plugin-helper/tickets.ts +122 -28
  76. package/shared/vendor/permission-audit/filter.ts +61 -0
  77. package/shared/vendor/permission-audit/index.ts +2 -0
  78. package/shared/vendor/permission-audit/shared.ts +119 -0
  79. package/shared/version.ts +1 -1
  80. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +0 -493
  81. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +0 -250
  82. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +0 -168
  83. package/client/vendor/paseo-plugin-helper/components/Button.tsx +0 -204
  84. package/client/vendor/paseo-plugin-helper/components/Card.tsx +0 -257
  85. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +0 -196
  86. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +0 -277
  87. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +0 -172
  88. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +0 -180
  89. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +0 -97
  90. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +0 -70
  91. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +0 -88
  92. package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +0 -142
  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 -132
  98. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +0 -87
  99. package/client/vendor/paseo-plugin-helper/components/Select.tsx +0 -283
  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 -26
  106. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +0 -225
  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 -30
  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 -368
  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/theme/flair.ts +0 -76
  121. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +0 -121
  122. package/client/vendor/paseo-plugin-helper/theme/index.ts +0 -7
  123. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +0 -178
  124. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +0 -213
  125. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +0 -200
  126. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +0 -57
  127. package/client/vendor/paseo-plugin-helper/tickets.tsx +0 -766
  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,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
  }