@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
@@ -0,0 +1,244 @@
1
+ import type { PluginRpcContract } from "./rpc";
2
+
3
+ /**
4
+ * Cleanup handle returned by a feature contribution.
5
+ *
6
+ * A disposer MUST be idempotent: calling it more than once must not throw and
7
+ * must not tear down anything twice. `composeFeatureModules` additionally
8
+ * wraps every disposer it receives so the *composed* teardown is idempotent
9
+ * even if an individual module forgets.
10
+ */
11
+ export type FeatureDisposer = () => void;
12
+
13
+ /**
14
+ * A self-contained, droppable unit of plugin functionality.
15
+ *
16
+ * A feature module owns one concern (custom pills, timeline telemetry,
17
+ * permission auditing, …) and exposes it through up to two contribution hooks:
18
+ * `contributeServer` for the daemon-side `PluginServerContext` and
19
+ * `contributeClient` for the client-side `PluginClientContext`. Both hooks are
20
+ * optional; a module may contribute to one side, both, or neither.
21
+ *
22
+ * Lifecycle contract
23
+ * ------------------
24
+ * - A contribution hook runs at most once per `contribute*` call on the
25
+ * composing plugin entry.
26
+ * - It may return a {@link FeatureDisposer} for cleanup, or `void` when it owns
27
+ * nothing to release.
28
+ * - Returned disposers MUST be idempotent (see {@link FeatureDisposer}).
29
+ * - The plugin entry composes modules with {@link composeFeatureModules} and
30
+ * returns the combined disposer to Paseo.
31
+ * - `contracts` is declarative metadata (name-keyed RPC contracts the module
32
+ * owns); it is not registered automatically. The composing entry still calls
33
+ * `server.handle(contract, handler)`.
34
+ *
35
+ * Context types are generic because `paseo-plugin-helper/shared` must not
36
+ * import the Paseo SDK. Type a module with the SDK contexts at the call site:
37
+ *
38
+ * ```ts
39
+ * import type { PluginClientContext } from "@getpaseo/plugin/client";
40
+ * import type { PluginServerContext } from "@getpaseo/plugin/server";
41
+ * import type { FeatureModule } from "paseo-plugin-helper/shared";
42
+ *
43
+ * export const customPills: FeatureModule<PluginServerContext, PluginClientContext> = {
44
+ * id: "custom-pills",
45
+ * contracts: { list: listCustomPillsRpc },
46
+ * contributeServer(server) {
47
+ * const { unsubscribe } = registerCustomPillsServer(server);
48
+ * return unsubscribe;
49
+ * },
50
+ * contributeClient(client) {
51
+ * const remove = registerCustomPills(client);
52
+ * return remove;
53
+ * },
54
+ * };
55
+ * ```
56
+ */
57
+ export interface FeatureModule<
58
+ TServerContext = unknown,
59
+ TClientContext = unknown,
60
+ > {
61
+ /** Stable, plugin-unique module id (e.g. `"custom-pills"`). */
62
+ readonly id: string;
63
+ /** Daemon-side contribution. Returns an idempotent disposer, or `void`. */
64
+ readonly contributeServer?: (server: TServerContext) => FeatureDisposer | void;
65
+ /** Client-side contribution. Returns an idempotent disposer, or `void`. */
66
+ readonly contributeClient?: (client: TClientContext) => FeatureDisposer | void;
67
+ /** RPC contracts this module owns, keyed by a local name. */
68
+ readonly contracts?: Record<string, PluginRpcContract>;
69
+ }
70
+
71
+ /**
72
+ * A composed set of feature modules. `contributeServer` / `contributeClient`
73
+ * run every module's hook for that side and return a single combined,
74
+ * idempotent disposer that tears the modules down in reverse order.
75
+ */
76
+ export interface FeatureComposition<
77
+ TServerContext = unknown,
78
+ TClientContext = unknown,
79
+ > {
80
+ /** Module ids in composition order. */
81
+ readonly ids: readonly string[];
82
+ /** Every declared contract, flattened in module order and de-duplicated. */
83
+ readonly contracts: readonly PluginRpcContract[];
84
+ /** Run all `contributeServer` hooks; returns the combined disposer. */
85
+ contributeServer(server: TServerContext): FeatureDisposer;
86
+ /** Run all `contributeClient` hooks; returns the combined disposer. */
87
+ contributeClient(client: TClientContext): FeatureDisposer;
88
+ }
89
+
90
+ function toIdempotentDisposer(disposer: FeatureDisposer): FeatureDisposer {
91
+ let called = false;
92
+ return () => {
93
+ if (called) return;
94
+ called = true;
95
+ disposer();
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Run a set of disposers in reverse (LIFO) order. Every disposer runs even if
101
+ * an earlier one throws; the first error is re-thrown after teardown finishes.
102
+ */
103
+ function disposeAll(disposers: readonly FeatureDisposer[]): void {
104
+ let hasError = false;
105
+ let firstError: unknown;
106
+ for (let i = disposers.length - 1; i >= 0; i -= 1) {
107
+ try {
108
+ disposers[i]!();
109
+ } catch (error) {
110
+ if (!hasError) {
111
+ hasError = true;
112
+ firstError = error;
113
+ }
114
+ }
115
+ }
116
+ if (hasError) throw firstError;
117
+ }
118
+
119
+ /**
120
+ * Collect the disposers produced by a per-side contribution hook. If a module
121
+ * throws mid-composition, already-contributed modules are disposed before the
122
+ * error is re-thrown so a failed composition never leaks partial state.
123
+ */
124
+ function runContributions<TServerContext, TClientContext>(
125
+ modules: readonly FeatureModule<TServerContext, TClientContext>[],
126
+ contribute: (
127
+ module: FeatureModule<TServerContext, TClientContext>,
128
+ ) => FeatureDisposer | void,
129
+ ): FeatureDisposer {
130
+ const disposers: FeatureDisposer[] = [];
131
+ let disposed = false;
132
+ const dispose = () => {
133
+ if (disposed) return;
134
+ disposed = true;
135
+ disposeAll(disposers);
136
+ };
137
+
138
+ try {
139
+ for (const module of modules) {
140
+ const result = contribute(module);
141
+ if (typeof result === "function") {
142
+ disposers.push(toIdempotentDisposer(result));
143
+ }
144
+ }
145
+ } catch (error) {
146
+ // Roll back what already mounted, but never let a failing disposer mask
147
+ // the contribution error that triggered the rollback.
148
+ try {
149
+ dispose();
150
+ } catch {
151
+ // ignored
152
+ }
153
+ throw error;
154
+ }
155
+
156
+ return dispose;
157
+ }
158
+
159
+ function assertUniqueModules<TServerContext, TClientContext>(
160
+ modules: readonly FeatureModule<TServerContext, TClientContext>[],
161
+ ): void {
162
+ const seen = new Set<string>();
163
+ for (const module of modules) {
164
+ const id = typeof module.id === "string" ? module.id.trim() : "";
165
+ if (!id) {
166
+ throw new Error("Feature module id must be a non-empty string");
167
+ }
168
+ if (seen.has(id)) {
169
+ throw new Error(`Duplicate feature module id "${id}"`);
170
+ }
171
+ seen.add(id);
172
+ }
173
+ }
174
+
175
+ function collectContracts<TServerContext, TClientContext>(
176
+ modules: readonly FeatureModule<TServerContext, TClientContext>[],
177
+ ): readonly PluginRpcContract[] {
178
+ const contracts: PluginRpcContract[] = [];
179
+ const seen = new Set<string>();
180
+ for (const module of modules) {
181
+ for (const contract of Object.values(module.contracts ?? {})) {
182
+ if (seen.has(contract.name)) {
183
+ throw new Error(
184
+ `Duplicate RPC contract "${contract.name}" across feature modules`,
185
+ );
186
+ }
187
+ seen.add(contract.name);
188
+ contracts.push(contract);
189
+ }
190
+ }
191
+ return Object.freeze(contracts);
192
+ }
193
+
194
+ /**
195
+ * Compose {@link FeatureModule}s into one contribution unit for a plugin entry.
196
+ *
197
+ * Modules run in array order on contribution and are disposed in reverse (LIFO)
198
+ * order. The returned combined disposer is idempotent: calling it twice runs
199
+ * teardown once and never throws the first error a second time.
200
+ *
201
+ * ```ts
202
+ * import type { PluginClientContext } from "@getpaseo/plugin/client";
203
+ * import type { PluginServerContext } from "@getpaseo/plugin/server";
204
+ * import { composeFeatureModules } from "paseo-plugin-helper/shared";
205
+ * import { customPills } from "./features/custom-pills";
206
+ * import { timelineTelemetry } from "./features/timeline-telemetry";
207
+ *
208
+ * const features = composeFeatureModules<PluginServerContext, PluginClientContext>([
209
+ * customPills,
210
+ * timelineTelemetry,
211
+ * ]);
212
+ *
213
+ * export function contributeServer(server: PluginServerContext) {
214
+ * return features.contributeServer(server);
215
+ * }
216
+ *
217
+ * export function contributeClient(client: PluginClientContext) {
218
+ * return features.contributeClient(client);
219
+ * }
220
+ * ```
221
+ *
222
+ * @throws if two modules share an `id` or declare the same RPC contract `name`.
223
+ */
224
+ export function composeFeatureModules<
225
+ TServerContext = unknown,
226
+ TClientContext = unknown,
227
+ >(
228
+ modules: readonly FeatureModule<TServerContext, TClientContext>[],
229
+ ): FeatureComposition<TServerContext, TClientContext> {
230
+ const list = [...modules];
231
+ assertUniqueModules(list);
232
+ const contracts = collectContracts(list);
233
+
234
+ return {
235
+ ids: Object.freeze(list.map((module) => module.id)),
236
+ contracts,
237
+ contributeServer(server) {
238
+ return runContributions(list, (module) => module.contributeServer?.(server));
239
+ },
240
+ contributeClient(client) {
241
+ return runContributions(list, (module) => module.contributeClient?.(client));
242
+ },
243
+ };
244
+ }
@@ -108,3 +108,30 @@ export function resolveForgeMark(
108
108
  const resolved = isForgeKind(explicit) ? explicit : forgeKindFromHost(host);
109
109
  return FORGE_MARKS[resolved];
110
110
  }
111
+
112
+ /**
113
+ * Official monochrome forge marks (Simple Icons, CC0 / project trademarks),
114
+ * authored on a 24x24 viewBox. Rendered as inline SVG through a data URI so a
115
+ * consumer needs no SVG dependency: exact vector marks on web/Electron, where
116
+ * Paseo renders React Native Web, and a Lucide fallback on native.
117
+ *
118
+ * Pure and React-free so it lives in `shared/` next to the resolver.
119
+ */
120
+ const FORGE_MARK_PATHS: Partial<Record<ForgeKind, string>> = {
121
+ codeberg:
122
+ "M11.999.747A11.974 11.974 0 0 0 0 12.75c0 2.254.635 4.465 1.833 6.376L11.837 6.19c.072-.092.251-.092.323 0l4.178 5.402h-2.992l.065.239h3.113l.882 1.138h-3.674l.103.374h3.86l.777 1.003h-4.358l.135.483h4.593l.695.894h-5.038l.165.589h5.326l.609.785h-5.717l.182.65h6.038l.562.727h-6.397l.183.65h6.717A12.003 12.003 0 0 0 24 12.75 11.977 11.977 0 0 0 11.999.747zm3.654 19.104.182.65h5.326c.173-.204.353-.433.513-.65zm.385 1.377.18.65h3.563c.233-.198.485-.428.712-.65zm.383 1.377.182.648h1.203c.356-.204.685-.412 1.042-.648zz",
123
+ forgejo:
124
+ "M16.7773 0c1.6018 0 2.9004 1.2986 2.9004 2.9005s-1.2986 2.9004-2.9004 2.9004c-1.0854 0-2.0315-.596-2.5288-1.4787H12.91c-2.3322 0-4.2272 1.8718-4.2649 4.195l-.0007 2.1175a7.0759 7.0759 0 0 1 4.148-1.4205l.1176-.001 1.3385.0002c.4973-.8827 1.4434-1.4788 2.5288-1.4788 1.6018 0 2.9004 1.2986 2.9004 2.9005s-1.2986 2.9004-2.9004 2.9004c-1.0854 0-2.0315-.596-2.5288-1.4787H12.91c-2.3322 0-4.2272 1.8718-4.2649 4.195l-.0007 2.319c.8827.4973 1.4788 1.4434 1.4788 2.5287 0 1.602-1.2986 2.9005-2.9005 2.9005-1.6018 0-2.9004-1.2986-2.9004-2.9005 0-1.0853.596-2.0314 1.4788-2.5287l-.0002-9.9831c0-3.887 3.1195-7.0453 6.9915-7.108l.1176-.001h1.3385C14.7458.5962 15.692 0 16.7773 0ZM7.2227 19.9052c-.6596 0-1.1943.5347-1.1943 1.1943s.5347 1.1943 1.1943 1.1943 1.1944-.5347 1.1944-1.1943-.5348-1.1943-1.1944-1.1943Zm9.5546-10.4644c-.6596 0-1.1944.5347-1.1944 1.1943s.5348 1.1943 1.1944 1.1943c.6596 0 1.1943-.5347 1.1943-1.1943s-.5347-1.1943-1.1943-1.1943Zm0-7.7346c-.6596 0-1.1944.5347-1.1944 1.1943s.5348 1.1943 1.1944 1.1943c.6596 0 1.1943-.5347 1.1943-1.1943s-.5347-1.1943-1.1943-1.1943Z",
125
+ gitea:
126
+ "M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z",
127
+ };
128
+
129
+ /** Inline SVG data URI for a custom mark, tinted with the resolved color. */
130
+ export function forgeMarkSource(kind: ForgeKind, color: string): { uri: string } | null {
131
+ const path = FORGE_MARK_PATHS[kind];
132
+ if (!path) return null;
133
+ const svg =
134
+ `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">` +
135
+ `<path fill="${color}" d="${path}"/></svg>`;
136
+ return { uri: `data:image/svg+xml,${encodeURIComponent(svg)}` };
137
+ }
@@ -1,5 +1,6 @@
1
1
  export * from "./types";
2
2
  export * from "./rpc";
3
+ export * from "./features";
3
4
  export * from "./formatters";
4
5
  export * from "./settings";
5
6
  export * from "./suite-settings";
@@ -5,64 +5,139 @@ import { defineContract } from "./rpc";
5
5
  // Label taxonomy & definitions (issues #122, #189, #200)
6
6
  // ---------------------------------------------------------------------------
7
7
 
8
+ // Canonical taxonomy is numberless (platform#247). Readers accept the legacy
9
+ // numeric spellings too (dual-read) while the board migrates; `LEGACY_*` arrays
10
+ // and `LABEL_ALIASES` keep the two spellings interchangeable.
8
11
  export const STATE_ORDER = [
12
+ "state/triage",
13
+ "state/wip",
14
+ "state/review",
15
+ "state/verify",
16
+ "state/done",
17
+ ] as const;
18
+ export type StateLabel = (typeof STATE_ORDER)[number];
19
+
20
+ export const LEGACY_STATE_ORDER = [
9
21
  "state/0-triage",
10
22
  "state/1-wip",
11
23
  "state/2-review",
12
24
  "state/3-verify",
13
25
  "state/4-done",
14
26
  ] as const;
15
- export type StateLabel = (typeof STATE_ORDER)[number];
16
27
 
17
28
  export const PRIORITY_ORDER = [
29
+ "priority/sos",
30
+ "priority/high",
31
+ "priority/normal",
32
+ "priority/low",
33
+ "priority/backburner",
34
+ ] as const;
35
+ export type PriorityLabel = (typeof PRIORITY_ORDER)[number];
36
+
37
+ export const LEGACY_PRIORITY_ORDER = [
18
38
  "priority/0-SOS",
19
39
  "priority/1-high",
20
40
  "priority/2-normal",
21
41
  "priority/3-low",
22
42
  "priority/4-backburner",
23
43
  ] as const;
24
- export type PriorityLabel = (typeof PRIORITY_ORDER)[number];
25
44
 
26
45
  export const ATTENTION_LABELS = [
46
+ "attention/orchestrator",
47
+ "attention/agent",
48
+ "attention/user",
49
+ "attention/ignore",
50
+ ] as const;
51
+ export type AttentionLabel = (typeof ATTENTION_LABELS)[number];
52
+
53
+ export const LEGACY_ATTENTION_LABELS = [
27
54
  "attention/0-orchestrator",
28
55
  "attention/1-agent",
29
56
  "attention/2-user",
30
57
  "attention/3-ignore",
31
58
  ] as const;
32
- export type AttentionLabel = (typeof ATTENTION_LABELS)[number];
33
59
 
34
60
  export const SPEC_LABELS = [
61
+ "spec/needed",
62
+ "spec/checklist",
63
+ "spec/approved",
64
+ ] as const;
65
+ export type SpecLabel = (typeof SPEC_LABELS)[number];
66
+
67
+ export const LEGACY_SPEC_LABELS = [
35
68
  "spec/0-needed",
36
69
  "spec/1-checklist",
37
70
  "spec/2-approved",
38
71
  ] as const;
39
- export type SpecLabel = (typeof SPEC_LABELS)[number];
40
72
 
41
- const STATE_SHORT: Record<string, string> = {
42
- "state/0-triage": "Triage",
43
- "state/1-wip": "WIP",
44
- "state/2-review": "Review",
45
- "state/3-verify": "Verify",
46
- "state/4-done": "Done",
73
+ function aliasPairs(
74
+ canonical: readonly string[],
75
+ legacy: readonly string[],
76
+ ): Record<string, string> {
77
+ const map: Record<string, string> = {};
78
+ canonical.forEach((name, i) => {
79
+ const legacyName = legacy[i];
80
+ if (legacyName) map[legacyName] = name;
81
+ });
82
+ return map;
83
+ }
84
+
85
+ /** Legacy numeric label -> canonical numberless label (all scopes). */
86
+ export const LABEL_ALIASES: Record<string, string> = {
87
+ ...aliasPairs(STATE_ORDER, LEGACY_STATE_ORDER),
88
+ ...aliasPairs(PRIORITY_ORDER, LEGACY_PRIORITY_ORDER),
89
+ ...aliasPairs(ATTENTION_LABELS, LEGACY_ATTENTION_LABELS),
90
+ ...aliasPairs(SPEC_LABELS, LEGACY_SPEC_LABELS),
91
+ "review/0-needed": "review/needed",
92
+ "review/1-changes-requested": "review/changes-requested",
93
+ "review/2-approved": "review/approved",
94
+ "format/0-needed": "format/needed",
95
+ "format/1-ok": "format/ok",
96
+ "size/0-cheap": "size/cheap",
97
+ "size/1-medium": "size/medium",
98
+ "size/2-expensive": "size/expensive",
99
+ "size/3-chunk": "size/chunk",
100
+ "linked/0-needs-split": "linked/needs-split",
101
+ "linked/1-peer": "linked/peer",
102
+ "linked/2-done": "linked/done",
103
+ "upstream/0-explore": "upstream/explore",
104
+ "upstream/1-blocked": "upstream/blocked",
105
+ "upstream/2-aligned": "upstream/aligned",
47
106
  };
48
107
 
49
- const PRIORITY_SHORT: Record<string, string> = {
50
- "priority/0-SOS": "SOS",
51
- "priority/1-high": "High",
52
- "priority/2-normal": "Normal",
53
- "priority/3-low": "Low",
54
- "priority/4-backburner": "Parked",
108
+ /** Canonical numberless spelling of a label; unknown labels pass through. */
109
+ export function canonicalLabel(label: string): string {
110
+ return LABEL_ALIASES[label] ?? label;
111
+ }
112
+
113
+ /** True when two spellings name the same scoped label under the alias map. */
114
+ export function sameLabel(a: string, b: string): boolean {
115
+ return canonicalLabel(a) === canonicalLabel(b);
116
+ }
117
+
118
+ const CANONICAL_SHORT: Record<string, string> = {
119
+ "state/triage": "Triage",
120
+ "state/wip": "WIP",
121
+ "state/review": "Review",
122
+ "state/verify": "Verify",
123
+ "state/done": "Done",
124
+ "priority/sos": "SOS",
125
+ "priority/high": "High",
126
+ "priority/normal": "Normal",
127
+ "priority/low": "Low",
128
+ "priority/backburner": "Parked",
55
129
  };
56
130
 
57
131
  /** Compact display alias for a scoped label ("state/1-wip" -> "WIP"). */
58
132
  export function shortLabelName(label: string): string {
59
- return STATE_SHORT[label] ?? PRIORITY_SHORT[label] ?? label;
133
+ return CANONICAL_SHORT[canonicalLabel(label)] ?? label;
60
134
  }
61
135
 
62
- /** The issue's current `state/*` label, or null when it carries none. */
136
+ /** The issue's current `state/*` label (canonical), or null when it carries none. */
63
137
  export function currentStateLabel(labels: string[]): string | null {
64
138
  for (const label of labels) {
65
- if ((STATE_ORDER as readonly string[]).includes(label)) return label;
139
+ const canonical = canonicalLabel(label);
140
+ if ((STATE_ORDER as readonly string[]).includes(canonical)) return canonical;
66
141
  }
67
142
  return null;
68
143
  }
@@ -70,15 +145,16 @@ export function currentStateLabel(labels: string[]): string | null {
70
145
  /** The issue's current `priority/*` label, defaulting to normal per spec §4.2. */
71
146
  export function currentPriorityLabel(labels: string[]): string {
72
147
  for (const label of labels) {
73
- if ((PRIORITY_ORDER as readonly string[]).includes(label)) return label;
148
+ const canonical = canonicalLabel(label);
149
+ if ((PRIORITY_ORDER as readonly string[]).includes(canonical)) return canonical;
74
150
  }
75
- return "priority/2-normal";
151
+ return "priority/normal";
76
152
  }
77
153
 
78
154
  /** Next `state/*` promotion step, or null when already done. */
79
155
  export function nextStateLabel(labels: string[]): string | null {
80
156
  const current = currentStateLabel(labels);
81
- if (!current) return "state/1-wip";
157
+ if (!current) return "state/wip";
82
158
  const idx = (STATE_ORDER as readonly string[]).indexOf(current);
83
159
  if (idx < 0 || idx + 1 >= STATE_ORDER.length) return null;
84
160
  return STATE_ORDER[idx + 1];
@@ -660,12 +736,6 @@ export const forgeContextContract = defineContract({
660
736
  output: ForgeContextOutputSchema,
661
737
  });
662
738
 
663
- export const forgeForgeContextContract = defineContract({
664
- name: "forge.forge-context",
665
- description: "Git-origin forge coordinates for a workspace, independent of issue queries",
666
- input: ForgeContextInputSchema,
667
- output: ForgeContextOutputSchema,
668
- });
669
739
 
670
740
  // ---------------------------------------------------------------------------
671
741
  // Agent envelope parsing
@@ -953,24 +1023,48 @@ const LABEL_DEFS: LabelDefinition[] = [
953
1023
  exclusive: true,
954
1024
  description: `Workflow state ${i}`,
955
1025
  })),
1026
+ ...LEGACY_STATE_ORDER.map((name, i): LabelDefinition => ({
1027
+ name,
1028
+ color: STATE_COLORS[i] ?? "#59636e",
1029
+ exclusive: true,
1030
+ description: `Workflow state ${i} (legacy numeric)`,
1031
+ })),
956
1032
  ...PRIORITY_ORDER.map((name, i): LabelDefinition => ({
957
1033
  name,
958
1034
  color: PRIORITY_COLORS[i] ?? "#59636e",
959
1035
  exclusive: true,
960
1036
  description: `Priority ${i}`,
961
1037
  })),
1038
+ ...LEGACY_PRIORITY_ORDER.map((name, i): LabelDefinition => ({
1039
+ name,
1040
+ color: PRIORITY_COLORS[i] ?? "#59636e",
1041
+ exclusive: true,
1042
+ description: `Priority ${i} (legacy numeric)`,
1043
+ })),
962
1044
  ...ATTENTION_LABELS.map((name): LabelDefinition => ({
963
1045
  name,
964
1046
  color: "#8250df",
965
1047
  exclusive: true,
966
1048
  description: "Who acts next",
967
1049
  })),
1050
+ ...LEGACY_ATTENTION_LABELS.map((name): LabelDefinition => ({
1051
+ name,
1052
+ color: "#8250df",
1053
+ exclusive: true,
1054
+ description: "Who acts next (legacy numeric)",
1055
+ })),
968
1056
  ...SPEC_LABELS.map((name): LabelDefinition => ({
969
1057
  name,
970
1058
  color: "#0e7c6b",
971
1059
  exclusive: true,
972
1060
  description: "Spec readiness",
973
1061
  })),
1062
+ ...LEGACY_SPEC_LABELS.map((name): LabelDefinition => ({
1063
+ name,
1064
+ color: "#0e7c6b",
1065
+ exclusive: true,
1066
+ description: "Spec readiness (legacy numeric)",
1067
+ })),
974
1068
  ];
975
1069
 
976
1070
  export function paseoLabelSet(): LabelDefinition[] {
@@ -0,0 +1,61 @@
1
+ import { isToolCallEntry, type AuditRecord, type PermissionDecision } from "./shared.js";
2
+
3
+ export type DecisionFilter = "all" | PermissionDecision;
4
+ export type AuditTypeFilter = "all" | "permission" | "tool_call";
5
+
6
+ export function formatAuditTime(iso: string): string {
7
+ const ms = Date.parse(iso);
8
+ if (Number.isNaN(ms)) return iso;
9
+ return new Date(ms).toLocaleString();
10
+ }
11
+
12
+ /**
13
+ * Middle-ellipsis for long values, keeping the leading key and the trailing
14
+ * identifier. `maxLength` is the content budget; the returned string can be one
15
+ * character longer once the ellipsis is inserted.
16
+ */
17
+ function truncateMiddle(text: string, maxLength = 120): string {
18
+ if (text.length <= maxLength) return text;
19
+ const head = Math.ceil(maxLength / 2);
20
+ const tail = Math.floor(maxLength / 2);
21
+ return `${text.slice(0, head)}…${text.slice(text.length - tail)}`;
22
+ }
23
+
24
+ export function summarizeAuditInput(input: unknown): string {
25
+ if (input === null || input === undefined) return "—";
26
+ if (typeof input === "string") return truncateMiddle(input);
27
+ try {
28
+ return truncateMiddle(JSON.stringify(input));
29
+ } catch {
30
+ return "—";
31
+ }
32
+ }
33
+
34
+ export function filterAuditEntries(
35
+ records: AuditRecord[],
36
+ decision: DecisionFilter,
37
+ search: string,
38
+ type: AuditTypeFilter = "all",
39
+ ): AuditRecord[] {
40
+ const needle = search.trim().toLowerCase();
41
+ return records.filter((record) => {
42
+ const toolCall = isToolCallEntry(record);
43
+ if (type === "permission" && toolCall) return false;
44
+ if (type === "tool_call" && !toolCall) return false;
45
+ // Decisions only exist on permission records; a decision filter excludes tool calls.
46
+ if (decision !== "all" && (toolCall || record.decision !== decision)) return false;
47
+ if (!needle) return true;
48
+ const haystack = [
49
+ record.name,
50
+ record.kind,
51
+ record.agentId,
52
+ record.agentModel ?? "",
53
+ summarizeAuditInput(record.input),
54
+ toolCall ? record.outcome : record.decision,
55
+ toolCall ? record.turnId ?? "" : "",
56
+ ]
57
+ .join(" ")
58
+ .toLowerCase();
59
+ return haystack.includes(needle);
60
+ });
61
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./shared.js";
2
+ export * from "./filter.js";