@xpufx/paseo-top 0.4.3 → 0.4.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/pill-labels.ts +30 -6
- package/client/pill.tsx +40 -61
- package/client/settings-events.ts +33 -0
- package/client/surface.tsx +6 -2
- package/client/vendor/paseo-plugin-helper/components/Badge.tsx +12 -0
- package/client/vendor/paseo-plugin-helper/components/Button.tsx +22 -2
- package/client/vendor/paseo-plugin-helper/components/Card.tsx +32 -0
- package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +17 -1
- package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +14 -1
- package/client/vendor/paseo-plugin-helper/components/InteractiveRow.tsx +142 -0
- package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +16 -2
- package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +7 -0
- package/client/vendor/paseo-plugin-helper/components/Select.tsx +131 -63
- package/client/vendor/paseo-plugin-helper/components/index.ts +1 -0
- package/client/vendor/paseo-plugin-helper/custom-pills.tsx +4 -3
- package/client/vendor/paseo-plugin-helper/host.ts +12 -58
- package/client/vendor/paseo-plugin-helper/index.ts +1 -0
- package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +17 -27
- package/client/vendor/paseo-plugin-helper/pill.tsx +68 -321
- package/client/vendor/paseo-plugin-helper/surface.tsx +62 -2
- package/client/vendor/paseo-plugin-helper/tickets.tsx +766 -0
- package/index.server.ts +7 -0
- package/package.json +11 -2
- package/server/vendor/paseo-plugin-helper/index.ts +2 -0
- package/server/vendor/paseo-plugin-helper/mcp-injection.ts +20 -2
- package/server/vendor/paseo-plugin-helper/plugins.ts +184 -19
- package/server/vendor/paseo-plugin-helper/tickets.ts +1281 -0
- package/shared/helper-version.ts +20 -0
- package/shared/multi-host.ts +11 -4
- package/shared/resources.ts +1 -1
- package/shared/vendor/paseo-plugin-helper/index.ts +1 -0
- package/shared/vendor/paseo-plugin-helper/tickets.ts +1010 -0
- package/shared/version.ts +1 -1
package/client/pill-labels.ts
CHANGED
|
@@ -56,6 +56,20 @@ export interface SegmentSnapshot {
|
|
|
56
56
|
worktreeLocationText?: string;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* First non-blank string wins, else the fallback. `??` only guards null and
|
|
61
|
+
* undefined, so a present-but-empty host field (blank `title`, `provider`,
|
|
62
|
+
* `branch`, `agentId`) slipped through as an empty label. On a button-shaped
|
|
63
|
+
* host an empty label renders icon-only, which reads as the pill collapsing to
|
|
64
|
+
* a skinny/empty line (xpufx-org/paseo#507).
|
|
65
|
+
*/
|
|
66
|
+
function firstNonBlank(fallback: string, ...values: Array<string | null | undefined>): string {
|
|
67
|
+
for (const value of values) {
|
|
68
|
+
if (typeof value === "string" && value.trim().length > 0) return value;
|
|
69
|
+
}
|
|
70
|
+
return fallback;
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
export interface TokenMetrics {
|
|
60
74
|
inputTokens?: number;
|
|
61
75
|
outputTokens?: number;
|
|
@@ -262,28 +276,38 @@ export function describeSegment(
|
|
|
262
276
|
const prefix = def?.shortLabel ? `${def.shortLabel} ` : "";
|
|
263
277
|
switch (item) {
|
|
264
278
|
case "branch":
|
|
265
|
-
return {
|
|
279
|
+
return {
|
|
280
|
+
icon,
|
|
281
|
+
iconTone: "accent",
|
|
282
|
+
text: firstNonBlank("--", data?.branch),
|
|
283
|
+
tone: "foreground",
|
|
284
|
+
};
|
|
266
285
|
case "worktree":
|
|
267
286
|
return {
|
|
268
287
|
icon,
|
|
269
288
|
iconTone: "accent",
|
|
270
|
-
text:
|
|
289
|
+
text: firstNonBlank("--", worktreeLocationText),
|
|
271
290
|
tone: "foreground",
|
|
272
291
|
};
|
|
273
292
|
case "agent_title":
|
|
274
|
-
return {
|
|
293
|
+
return {
|
|
294
|
+
icon,
|
|
295
|
+
iconTone: "accent",
|
|
296
|
+
text: firstNonBlank("Agent", agent?.title),
|
|
297
|
+
tone: "foreground",
|
|
298
|
+
};
|
|
275
299
|
case "agent":
|
|
276
300
|
return {
|
|
277
301
|
icon,
|
|
278
302
|
iconTone: "accent",
|
|
279
|
-
text: agent?.model
|
|
303
|
+
text: firstNonBlank("Agent", agent?.model, agent?.provider),
|
|
280
304
|
tone: "foreground",
|
|
281
305
|
};
|
|
282
306
|
case "agent_provider":
|
|
283
307
|
return {
|
|
284
308
|
icon,
|
|
285
309
|
iconTone: "accent",
|
|
286
|
-
text:
|
|
310
|
+
text: firstNonBlank("Provider", agent?.provider),
|
|
287
311
|
tone: "foreground",
|
|
288
312
|
};
|
|
289
313
|
case "agent_activity": {
|
|
@@ -299,7 +323,7 @@ export function describeSegment(
|
|
|
299
323
|
return {
|
|
300
324
|
icon,
|
|
301
325
|
iconTone: "accent",
|
|
302
|
-
text: agentId && agentId.length > 7 ? agentId.slice(0, 7) :
|
|
326
|
+
text: agentId && agentId.length > 7 ? agentId.slice(0, 7) : firstNonBlank("--", agentId),
|
|
303
327
|
tone: "foreground",
|
|
304
328
|
};
|
|
305
329
|
case "load":
|
package/client/pill.tsx
CHANGED
|
@@ -28,16 +28,14 @@ import {
|
|
|
28
28
|
Badge,
|
|
29
29
|
Icon,
|
|
30
30
|
AboutSection,
|
|
31
|
-
CustomPillBody,
|
|
32
31
|
CustomPillModalContent,
|
|
33
32
|
useRpcQuery,
|
|
34
33
|
usePluginSettings,
|
|
35
34
|
usePluginTheme,
|
|
36
35
|
getStatusColor,
|
|
37
36
|
triggerHaptic,
|
|
38
|
-
|
|
37
|
+
type HostPillProps,
|
|
39
38
|
type RenderModalProps,
|
|
40
|
-
type RenderPillProps,
|
|
41
39
|
type KeyValueProps,
|
|
42
40
|
type CardHeaderProps,
|
|
43
41
|
type BadgeProps,
|
|
@@ -47,8 +45,8 @@ import {
|
|
|
47
45
|
formatUptime,
|
|
48
46
|
resolveMetricStatus,
|
|
49
47
|
type MetricThresholds,
|
|
50
|
-
type CustomPillState,
|
|
51
48
|
} from "../shared/vendor/paseo-plugin-helper/index";
|
|
49
|
+
import { PermissionAuditView } from "permission-audit/client";
|
|
52
50
|
import {
|
|
53
51
|
getSystemResourcesRpc,
|
|
54
52
|
topSettingsContract,
|
|
@@ -72,6 +70,7 @@ import {
|
|
|
72
70
|
} from "../shared/resources";
|
|
73
71
|
import { PLUGIN_VERSION } from "../shared/version";
|
|
74
72
|
import { TopDashboardSurface } from "./surface";
|
|
73
|
+
import { addSettingsListener, removeSettingsListener, notifySettingsChanged } from "./settings-events";
|
|
75
74
|
import { useTopResourceQuery, useCustomPillsQuery } from "./resources-query";
|
|
76
75
|
import { ChoiceChips } from "./settings-ui";
|
|
77
76
|
import {
|
|
@@ -140,6 +139,7 @@ const MEM_THRESHOLDS: MetricThresholds = { warning: 70, danger: 85 };
|
|
|
140
139
|
const TABS = [
|
|
141
140
|
{ id: "system", label: "System", shortLabel: "System", icon: "Activity" },
|
|
142
141
|
{ id: "context", label: "Workspace", shortLabel: "Workspace", icon: "GitBranch" },
|
|
142
|
+
{ id: "permissions", label: "Permissions", shortLabel: "Permissions", icon: "ShieldCheck" },
|
|
143
143
|
{ id: "settings", label: "Settings", shortLabel: "Settings", icon: "Sliders" },
|
|
144
144
|
{ id: "about", label: "About", shortLabel: "About", icon: "Info" },
|
|
145
145
|
];
|
|
@@ -177,7 +177,7 @@ function getMetricColors(
|
|
|
177
177
|
// so pill call sites keep a single import path.
|
|
178
178
|
export type { PillItemType } from "./pill-labels";
|
|
179
179
|
|
|
180
|
-
export type ModalTab = "system" | "context" | "settings" | "about";
|
|
180
|
+
export type ModalTab = "system" | "context" | "permissions" | "settings" | "about";
|
|
181
181
|
|
|
182
182
|
export function getItemTab(item: PillItemType): "system" | "context" {
|
|
183
183
|
switch (item) {
|
|
@@ -448,26 +448,8 @@ function PillItemContent({
|
|
|
448
448
|
);
|
|
449
449
|
}
|
|
450
450
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
let lastNotifiedSettings: TopSettings | null = null;
|
|
454
|
-
|
|
455
|
-
export function notifySettingsChanged(settings: TopSettings) {
|
|
456
|
-
if (!shouldEmitSnapshotUpdate(
|
|
457
|
-
lastNotifiedSettings as unknown as Record<string, unknown> | null,
|
|
458
|
-
settings as unknown as Record<string, unknown>,
|
|
459
|
-
)) {
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
lastNotifiedSettings = settings;
|
|
463
|
-
for (const listener of settingsListeners) {
|
|
464
|
-
try {
|
|
465
|
-
listener(settings);
|
|
466
|
-
} catch {
|
|
467
|
-
// Ignore listener errors
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
}
|
|
451
|
+
export { notifySettingsChanged } from "./settings-events";
|
|
452
|
+
type SettingsListener = import("./settings-events").SettingsListener;
|
|
471
453
|
|
|
472
454
|
function PillOffline() {
|
|
473
455
|
const { colors } = usePluginTheme();
|
|
@@ -521,9 +503,8 @@ interface PillSegmentProps {
|
|
|
521
503
|
}
|
|
522
504
|
|
|
523
505
|
/**
|
|
524
|
-
* The
|
|
525
|
-
*
|
|
526
|
-
* segment must not nest a second interaction handler.
|
|
506
|
+
* The segment must not nest a second interaction handler: on the host-rendered
|
|
507
|
+
* pill it sits inside the host's own pressable.
|
|
527
508
|
*/
|
|
528
509
|
function PillSegment({
|
|
529
510
|
item,
|
|
@@ -584,7 +565,21 @@ function AllInOnePill({
|
|
|
584
565
|
);
|
|
585
566
|
}
|
|
586
567
|
|
|
587
|
-
|
|
568
|
+
/**
|
|
569
|
+
* Props the pill body views used to receive from the helper's `renderPill`
|
|
570
|
+
* option, which the Paseo 0.9 host shape removed: the host renders the pill
|
|
571
|
+
* body itself, so nothing wires these views any more. The shape is kept
|
|
572
|
+
* because `pill-render.test.tsx` still regression-tests their mobile layout
|
|
573
|
+
* (#507) and that coverage is worth more than the dead wiring.
|
|
574
|
+
*/
|
|
575
|
+
interface PillBodyProps extends HostPillProps {
|
|
576
|
+
isOpen: boolean;
|
|
577
|
+
open(payload?: ModalTab): void;
|
|
578
|
+
close(): void;
|
|
579
|
+
toggle(payload?: ModalTab): void;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export interface SingleItemPillViewProps extends PillBodyProps {
|
|
588
583
|
item: PillItemType;
|
|
589
584
|
defaultTab?: ModalTab;
|
|
590
585
|
}
|
|
@@ -669,7 +664,7 @@ export function SingleItemPillView({
|
|
|
669
664
|
);
|
|
670
665
|
}
|
|
671
666
|
|
|
672
|
-
function PillView({ isOpen, open, workspaceId, agentId }:
|
|
667
|
+
function PillView({ isOpen, open, workspaceId, agentId }: PillBodyProps) {
|
|
673
668
|
// No background settings poll here: the hook re-verifies on mount and
|
|
674
669
|
// window focus, and mutations invalidate the shared settings cache.
|
|
675
670
|
// Polling from every mounted pill/modal/surface multiplied daemon reads
|
|
@@ -1685,6 +1680,10 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
|
|
|
1685
1680
|
</>
|
|
1686
1681
|
)}
|
|
1687
1682
|
|
|
1683
|
+
{activeTab === "permissions" && (
|
|
1684
|
+
<PermissionAuditView variant="compact" />
|
|
1685
|
+
)}
|
|
1686
|
+
|
|
1688
1687
|
{activeTab === "about" && (
|
|
1689
1688
|
<AboutSection
|
|
1690
1689
|
name="paseo-top"
|
|
@@ -1731,17 +1730,6 @@ function ResourceModal({ theme, workspaceId, agentId, initialTab, payload }: Res
|
|
|
1731
1730
|
);
|
|
1732
1731
|
}
|
|
1733
1732
|
|
|
1734
|
-
interface LiveCustomPillViewProps {
|
|
1735
|
-
pillId: string;
|
|
1736
|
-
initial: CustomPillState;
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
function LiveCustomPillView({ pillId, initial }: LiveCustomPillViewProps) {
|
|
1740
|
-
const { data } = useCustomPillsQuery();
|
|
1741
|
-
const liveState = data?.pills.find((p) => p.id === pillId) ?? initial;
|
|
1742
|
-
return <CustomPillBody state={liveState} />;
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
1733
|
interface LiveCustomPillModalProps {
|
|
1746
1734
|
pillId: string;
|
|
1747
1735
|
initial: CustomPillStateOutput;
|
|
@@ -1856,12 +1844,6 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
1856
1844
|
modalTitle: "Host System Resources",
|
|
1857
1845
|
modalIcon: "Activity",
|
|
1858
1846
|
icon: "Cpu",
|
|
1859
|
-
resolveDefaultPayload: ({ agentId }) => {
|
|
1860
|
-
if (latestSettings.pillMode === "all") {
|
|
1861
|
-
return latestSettings.defaultTab;
|
|
1862
|
-
}
|
|
1863
|
-
return currentCycleTabByAgent.get(agentId) ?? latestSettings.defaultTab;
|
|
1864
|
-
},
|
|
1865
1847
|
resolveLabel: async (ctx) => {
|
|
1866
1848
|
const snap = await liveSnapshotFor(ctx);
|
|
1867
1849
|
const items = enabledItemsForSettings(latestSettings);
|
|
@@ -1878,7 +1860,6 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
1878
1860
|
};
|
|
1879
1861
|
},
|
|
1880
1862
|
refreshIntervalMs: 3000,
|
|
1881
|
-
renderPill: (props) => <PillView {...props} />,
|
|
1882
1863
|
renderModal: (props) => <ResourceModal {...props} />,
|
|
1883
1864
|
});
|
|
1884
1865
|
activePills.set("paseo-top", cleanup);
|
|
@@ -2083,16 +2064,8 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
2083
2064
|
modalTitle: pillDef.modalTitle,
|
|
2084
2065
|
icon: pillDef.icon,
|
|
2085
2066
|
modalIcon: pillDef.icon,
|
|
2086
|
-
resolveDefaultPayload: () => pillDef.defaultTab,
|
|
2087
2067
|
resolveLabel: singleItemLabelResolver(pillDef.item),
|
|
2088
2068
|
refreshIntervalMs: 5000,
|
|
2089
|
-
renderPill: (props) => (
|
|
2090
|
-
<SingleItemPillView
|
|
2091
|
-
item={pillDef.item}
|
|
2092
|
-
defaultTab={pillDef.defaultTab}
|
|
2093
|
-
{...props}
|
|
2094
|
-
/>
|
|
2095
|
-
),
|
|
2096
2069
|
renderModal: (props) => (
|
|
2097
2070
|
<ResourceModal initialTab={pillDef.defaultTab} {...props} />
|
|
2098
2071
|
),
|
|
@@ -2104,7 +2077,7 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
2104
2077
|
}
|
|
2105
2078
|
|
|
2106
2079
|
// Register settings listener
|
|
2107
|
-
|
|
2080
|
+
addSettingsListener(syncPills);
|
|
2108
2081
|
|
|
2109
2082
|
// Initial sync synchronously
|
|
2110
2083
|
syncPills(topSettingsContract.defaultSettings);
|
|
@@ -2185,7 +2158,6 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
2185
2158
|
);
|
|
2186
2159
|
},
|
|
2187
2160
|
refreshIntervalMs: 5000,
|
|
2188
|
-
renderPill: () => <LiveCustomPillView pillId={pill.id} initial={pill} />,
|
|
2189
2161
|
renderModal: () => (
|
|
2190
2162
|
<LiveCustomPillModal pillId={pill.id} initial={pill} />
|
|
2191
2163
|
),
|
|
@@ -2206,7 +2178,7 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
2206
2178
|
const onSettingsChanged = () => {
|
|
2207
2179
|
void syncCustomPills();
|
|
2208
2180
|
};
|
|
2209
|
-
|
|
2181
|
+
addSettingsListener(onSettingsChanged);
|
|
2210
2182
|
|
|
2211
2183
|
let disposed = false;
|
|
2212
2184
|
const cleanup = () => {
|
|
@@ -2218,8 +2190,8 @@ export function contributeClient(client: ComposerPillRegistrar | PluginClientCon
|
|
|
2218
2190
|
cleanupSidebar = null;
|
|
2219
2191
|
}
|
|
2220
2192
|
clearInterval(customPillInterval);
|
|
2221
|
-
|
|
2222
|
-
|
|
2193
|
+
removeSettingsListener(syncPills);
|
|
2194
|
+
removeSettingsListener(onSettingsChanged);
|
|
2223
2195
|
for (const cleanup of activePills.values()) {
|
|
2224
2196
|
cleanup();
|
|
2225
2197
|
}
|
|
@@ -2252,6 +2224,11 @@ const styles = {
|
|
|
2252
2224
|
overflow: "hidden",
|
|
2253
2225
|
flexShrink: 1,
|
|
2254
2226
|
minWidth: 0,
|
|
2227
|
+
// The pill label is one short line; without a height floor a body that
|
|
2228
|
+
// momentarily renders no text (loading, or a blank host field) collapses to
|
|
2229
|
+
// an unreadable skinny line on mobile (xpufx-org/paseo#507). The host
|
|
2230
|
+
// composer pill is 32pt; keep the inner body at least a line tall.
|
|
2231
|
+
minHeight: 20,
|
|
2255
2232
|
},
|
|
2256
2233
|
pillText: {
|
|
2257
2234
|
fontSize: 11,
|
|
@@ -2417,6 +2394,8 @@ const styles = {
|
|
|
2417
2394
|
overflow: "hidden",
|
|
2418
2395
|
flexShrink: 1,
|
|
2419
2396
|
minWidth: 0,
|
|
2397
|
+
// See pillContainer: the combined pill must keep a line-height floor too.
|
|
2398
|
+
minHeight: 20,
|
|
2420
2399
|
},
|
|
2421
2400
|
dividerText: {
|
|
2422
2401
|
fontSize: 10,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { shouldEmitSnapshotUpdate } from "./vendor/paseo-plugin-helper/index";
|
|
2
|
+
import type { TopSettings } from "../shared/resources";
|
|
3
|
+
|
|
4
|
+
export type SettingsListener = (settings: TopSettings) => void;
|
|
5
|
+
const settingsListeners = new Set<SettingsListener>();
|
|
6
|
+
let lastNotifiedSettings: TopSettings | null = null;
|
|
7
|
+
|
|
8
|
+
export function addSettingsListener(listener: SettingsListener): void {
|
|
9
|
+
settingsListeners.add(listener);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function removeSettingsListener(listener: SettingsListener): void {
|
|
13
|
+
settingsListeners.delete(listener);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function notifySettingsChanged(settings: TopSettings): void {
|
|
17
|
+
if (
|
|
18
|
+
!shouldEmitSnapshotUpdate(
|
|
19
|
+
lastNotifiedSettings as unknown as Record<string, unknown> | null,
|
|
20
|
+
settings as unknown as Record<string, unknown>,
|
|
21
|
+
)
|
|
22
|
+
) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
lastNotifiedSettings = settings;
|
|
26
|
+
for (const listener of settingsListeners) {
|
|
27
|
+
try {
|
|
28
|
+
listener(settings);
|
|
29
|
+
} catch {
|
|
30
|
+
// Ignore listener errors
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
package/client/surface.tsx
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
usePluginTheme,
|
|
19
19
|
} from "./vendor/paseo-plugin-helper/index";
|
|
20
20
|
import { formatBytes, formatUptime } from "../shared/vendor/paseo-plugin-helper/index";
|
|
21
|
+
import { PermissionAuditView } from "permission-audit/client";
|
|
21
22
|
import {
|
|
22
23
|
topSettingsContract,
|
|
23
24
|
checkboxesFromTarget,
|
|
@@ -27,16 +28,17 @@ import {
|
|
|
27
28
|
type TopSettings,
|
|
28
29
|
} from "../shared/resources";
|
|
29
30
|
import { PLUGIN_VERSION } from "../shared/version";
|
|
30
|
-
import { notifySettingsChanged } from "./
|
|
31
|
+
import { notifySettingsChanged } from "./settings-events";
|
|
31
32
|
import { ChoiceChips } from "./settings-ui";
|
|
32
33
|
import { useTopResourceQuery } from "./resources-query";
|
|
33
34
|
import { FleetView } from "./multi-host-view";
|
|
34
35
|
|
|
35
|
-
type SurfaceTab = "system" | "fleet" | "settings" | "about";
|
|
36
|
+
type SurfaceTab = "system" | "fleet" | "permissions" | "settings" | "about";
|
|
36
37
|
|
|
37
38
|
const TABS = [
|
|
38
39
|
{ id: "system", label: "Activity", shortLabel: "Activity", icon: "Activity" },
|
|
39
40
|
{ id: "fleet", label: "Fleet", shortLabel: "Fleet", icon: "Server" },
|
|
41
|
+
{ id: "permissions", label: "Permissions", shortLabel: "Permissions", icon: "ShieldCheck" },
|
|
40
42
|
{ id: "settings", label: "Settings", shortLabel: "Settings", icon: "Sliders" },
|
|
41
43
|
{ id: "about", label: "About", shortLabel: "About", icon: "Info" },
|
|
42
44
|
];
|
|
@@ -174,6 +176,8 @@ export function TopDashboardSurface(_props: PluginSurfaceProps) {
|
|
|
174
176
|
|
|
175
177
|
{activeTab === "fleet" && <FleetView />}
|
|
176
178
|
|
|
179
|
+
{activeTab === "permissions" && <PermissionAuditView variant="compact" />}
|
|
180
|
+
|
|
177
181
|
{activeTab === "settings" && (
|
|
178
182
|
<Stack gap={12}>
|
|
179
183
|
<Card variant="elevated">
|
|
@@ -112,6 +112,9 @@ export function Badge({
|
|
|
112
112
|
>
|
|
113
113
|
{renderIcon()}
|
|
114
114
|
<Text
|
|
115
|
+
accessibilityLabel={label}
|
|
116
|
+
numberOfLines={1}
|
|
117
|
+
ellipsizeMode="tail"
|
|
115
118
|
style={[
|
|
116
119
|
styles.text,
|
|
117
120
|
{
|
|
@@ -128,6 +131,7 @@ export function Badge({
|
|
|
128
131
|
text={label}
|
|
129
132
|
query={highlightQuery}
|
|
130
133
|
fuzzyFallback={highlightFuzzyFallback}
|
|
134
|
+
numberOfLines={1}
|
|
131
135
|
/>
|
|
132
136
|
) : (
|
|
133
137
|
label
|
|
@@ -144,9 +148,17 @@ const styles = StyleSheet.create({
|
|
|
144
148
|
alignSelf: "flex-start",
|
|
145
149
|
borderWidth: 1,
|
|
146
150
|
gap: 4,
|
|
151
|
+
// A badge carries operator-supplied strings (ids, paths, refs, model names)
|
|
152
|
+
// of unbounded length. Yoga defaults `flexShrink` to 0, so without this the
|
|
153
|
+
// chip insists on its full text width and pushes its row past the viewport
|
|
154
|
+
// instead of truncating.
|
|
155
|
+
flexShrink: 1,
|
|
156
|
+
maxWidth: "100%",
|
|
147
157
|
},
|
|
148
158
|
text: {
|
|
149
159
|
fontWeight: "600",
|
|
160
|
+
// Truncation only engages if the label itself is allowed to compress.
|
|
161
|
+
flexShrink: 1,
|
|
150
162
|
},
|
|
151
163
|
dot: {
|
|
152
164
|
width: 6,
|
|
@@ -112,13 +112,25 @@ export function Button({
|
|
|
112
112
|
|
|
113
113
|
const attentionMode = resolveButtonAttentionMode(attention);
|
|
114
114
|
|
|
115
|
+
// Accessibility touch targets are met with `hitSlop`, which expands the
|
|
116
|
+
// tappable area without moving a single neighbouring pixel. They are NOT met
|
|
117
|
+
// by inflating the painted box: a `minHeight` floor forced `size="sm"` to
|
|
118
|
+
// render 40px tall on mobile against a ~26px recipe, because the floor was
|
|
119
|
+
// computed from the touch target rather than from the size (#647).
|
|
120
|
+
//
|
|
121
|
+
// The slop has to be derived from this button's own height, not a fixed 32 —
|
|
122
|
+
// a 32px baseline silently under-serves every smaller recipe, so `sm` got
|
|
123
|
+
// both a too-tall box and too little slop.
|
|
124
|
+
const contentHeight = Math.round(fontSize * 1.2) + py * 2;
|
|
125
|
+
const hitSlopSide = Math.max(0, (touchTargetMin - contentHeight) / 2);
|
|
126
|
+
|
|
115
127
|
const pressable = (
|
|
116
128
|
<Pressable
|
|
117
129
|
onPress={onPress}
|
|
118
130
|
disabled={disabled || loading}
|
|
119
131
|
accessibilityRole={accessibilityRole}
|
|
120
132
|
accessibilityLabel={accessibilityLabel || label}
|
|
121
|
-
hitSlop={
|
|
133
|
+
hitSlop={hitSlopSide}
|
|
122
134
|
style={({ pressed }) => [
|
|
123
135
|
styles.base,
|
|
124
136
|
{
|
|
@@ -128,7 +140,6 @@ export function Button({
|
|
|
128
140
|
borderRadius: radius,
|
|
129
141
|
paddingVertical: py,
|
|
130
142
|
paddingHorizontal: px,
|
|
131
|
-
minHeight: Math.max(30, touchTargetMin - 4),
|
|
132
143
|
opacity: disabled ? 0.45 : 1,
|
|
133
144
|
},
|
|
134
145
|
style,
|
|
@@ -143,6 +154,8 @@ export function Button({
|
|
|
143
154
|
{iconPosition === "left" && renderIcon()}
|
|
144
155
|
{label ? (
|
|
145
156
|
<Text
|
|
157
|
+
numberOfLines={1}
|
|
158
|
+
ellipsizeMode="tail"
|
|
146
159
|
style={[
|
|
147
160
|
styles.text,
|
|
148
161
|
{
|
|
@@ -176,9 +189,16 @@ const styles = StyleSheet.create({
|
|
|
176
189
|
alignItems: "center",
|
|
177
190
|
justifyContent: "center",
|
|
178
191
|
gap: 6,
|
|
192
|
+
// Labels are caller-supplied and unbounded (issue titles, branch names,
|
|
193
|
+
// sort labels). Yoga defaults `flexShrink` to 0, so an unconstrained
|
|
194
|
+
// button claims its full label width and overflows narrow viewports.
|
|
195
|
+
flexShrink: 1,
|
|
196
|
+
maxWidth: "100%",
|
|
179
197
|
},
|
|
180
198
|
text: {
|
|
181
199
|
fontWeight: "600",
|
|
182
200
|
textAlign: "center",
|
|
201
|
+
// The label must be allowed to compress for `numberOfLines` to engage.
|
|
202
|
+
flexShrink: 1,
|
|
183
203
|
},
|
|
184
204
|
});
|
|
@@ -30,6 +30,11 @@ export interface CardHeaderProps {
|
|
|
30
30
|
highlightQuery?: string;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
// TEMPORARY DIAGNOSTIC (#555) — one-shot probe logging the host icon the
|
|
34
|
+
// first CardHeader render resolves, right before it is used; strip once the
|
|
35
|
+
// missing-binding cause is confirmed.
|
|
36
|
+
let cardHeaderIconProbeFired = false;
|
|
37
|
+
|
|
33
38
|
export function CardHeader({
|
|
34
39
|
title,
|
|
35
40
|
subtitle,
|
|
@@ -43,6 +48,33 @@ export function CardHeader({
|
|
|
43
48
|
highlightQuery,
|
|
44
49
|
}: CardHeaderProps) {
|
|
45
50
|
const { Icon } = getClientHost();
|
|
51
|
+
// TEMPORARY DIAGNOSTIC (#555) — strip once the missing-binding cause is
|
|
52
|
+
// confirmed. A probe that throws must never crash the header itself.
|
|
53
|
+
try {
|
|
54
|
+
if (!cardHeaderIconProbeFired) {
|
|
55
|
+
cardHeaderIconProbeFired = true;
|
|
56
|
+
const describe = (value: unknown): string => {
|
|
57
|
+
if (value === undefined) return "undefined";
|
|
58
|
+
if (value === null) return "null";
|
|
59
|
+
if (typeof value === "function") {
|
|
60
|
+
const named = (value as { displayName?: string; name?: string }).displayName
|
|
61
|
+
?? (value as { name?: string }).name;
|
|
62
|
+
return `function(${named || "(anonymous)"})`;
|
|
63
|
+
}
|
|
64
|
+
if (typeof value === "object") {
|
|
65
|
+
const symbolTag = (value as { $$typeof?: unknown }).$$typeof;
|
|
66
|
+
return `object($$typeof=${String(symbolTag)}, render=${typeof (value as { render?: unknown }).render})`;
|
|
67
|
+
}
|
|
68
|
+
return typeof value;
|
|
69
|
+
};
|
|
70
|
+
console.warn(
|
|
71
|
+
"[2fado] CardHeader probe: icon=", icon ?? "(no icon prop)",
|
|
72
|
+
"resolved host Icon=", `typeofIcon=${typeof Icon}`, describe(Icon),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
} catch (err) {
|
|
76
|
+
console.warn("[2fado] CardHeader probe failed:", err);
|
|
77
|
+
}
|
|
46
78
|
const { colors, flair, typography, padding, isCompact } = usePluginTheme();
|
|
47
79
|
|
|
48
80
|
return (
|
|
@@ -104,7 +104,11 @@ export function DataTable<T>({
|
|
|
104
104
|
: styles.alignLeft,
|
|
105
105
|
]}
|
|
106
106
|
>
|
|
107
|
-
<Text
|
|
107
|
+
<Text
|
|
108
|
+
numberOfLines={1}
|
|
109
|
+
ellipsizeMode="tail"
|
|
110
|
+
style={[styles.headerText, { color: colors.foregroundMuted }]}
|
|
111
|
+
>
|
|
108
112
|
{col.header}
|
|
109
113
|
</Text>
|
|
110
114
|
</View>
|
|
@@ -162,6 +166,11 @@ const styles = StyleSheet.create({
|
|
|
162
166
|
},
|
|
163
167
|
cell: {
|
|
164
168
|
justifyContent: "center",
|
|
169
|
+
// Yoga defaults `flexShrink` to 0, so without this a cell whose content
|
|
170
|
+
// carries a long string (a title, a path) refuses to give up width and
|
|
171
|
+
// squeezes its sibling columns instead of truncating.
|
|
172
|
+
flexShrink: 1,
|
|
173
|
+
maxWidth: "100%",
|
|
165
174
|
},
|
|
166
175
|
alignLeft: {
|
|
167
176
|
alignItems: "flex-start",
|
|
@@ -177,6 +186,7 @@ const styles = StyleSheet.create({
|
|
|
177
186
|
fontWeight: "600",
|
|
178
187
|
textTransform: "uppercase",
|
|
179
188
|
letterSpacing: 0.5,
|
|
189
|
+
flexShrink: 1,
|
|
180
190
|
},
|
|
181
191
|
compactContainer: {
|
|
182
192
|
gap: 8,
|
|
@@ -190,11 +200,17 @@ const styles = StyleSheet.create({
|
|
|
190
200
|
flexDirection: "row",
|
|
191
201
|
justifyContent: "space-between",
|
|
192
202
|
alignItems: "center",
|
|
203
|
+
gap: 8,
|
|
193
204
|
},
|
|
194
205
|
compactHeader: {
|
|
195
206
|
fontSize: 12,
|
|
207
|
+
flexShrink: 1,
|
|
196
208
|
},
|
|
197
209
|
compactValue: {
|
|
198
210
|
alignItems: "flex-end",
|
|
211
|
+
// The value side carries the actual data, so it takes the compressible
|
|
212
|
+
// slot and the fixed label beside it keeps its intrinsic width.
|
|
213
|
+
flexShrink: 1,
|
|
214
|
+
maxWidth: "100%",
|
|
199
215
|
},
|
|
200
216
|
});
|
|
@@ -52,7 +52,14 @@ export function InlineButton({
|
|
|
52
52
|
]}
|
|
53
53
|
>
|
|
54
54
|
{typeof icon === "string" ? <Icon name={icon} size={13} color={colors.accent} /> : icon}
|
|
55
|
-
<Text
|
|
55
|
+
<Text
|
|
56
|
+
accessibilityLabel={label}
|
|
57
|
+
numberOfLines={1}
|
|
58
|
+
ellipsizeMode="tail"
|
|
59
|
+
style={[styles.text, { color: colors.accent }, textStyle]}
|
|
60
|
+
>
|
|
61
|
+
{label}
|
|
62
|
+
</Text>
|
|
56
63
|
</Pressable>
|
|
57
64
|
);
|
|
58
65
|
}
|
|
@@ -67,9 +74,15 @@ const styles = StyleSheet.create({
|
|
|
67
74
|
paddingHorizontal: 3,
|
|
68
75
|
paddingVertical: 2,
|
|
69
76
|
borderRadius: 4,
|
|
77
|
+
// Caller-supplied labels are unbounded; without a shrink budget the control
|
|
78
|
+
// claims its full text width and overflows narrow viewports.
|
|
79
|
+
flexShrink: 1,
|
|
80
|
+
maxWidth: "100%",
|
|
70
81
|
},
|
|
71
82
|
text: {
|
|
72
83
|
fontSize: 12,
|
|
73
84
|
fontWeight: "600",
|
|
85
|
+
// The label must be allowed to compress for `numberOfLines` to engage.
|
|
86
|
+
flexShrink: 1,
|
|
74
87
|
},
|
|
75
88
|
});
|