Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/main/processes/process-action-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ function hasStableTargetIdentity(target: ProcessIdentity | undefined): boolean {
return target?.startedAtStatus === FieldStatus.FIELD_STATUS_OK;
}

/**
* True for MoStats itself: the main process or any of its direct child helpers
* (renderer, GPU, utility), so neither can be signaled into self-destabilizing
* the app. Helper PIDs differ from the main PID, but their parent is it.
*/
export function isSelfProcess(row: ProcessRow, selfPid: number): boolean {
if ((row.identity?.pid ?? 0) === selfPid) {
return true;
}
return row.statics?.parentStatus === FieldStatus.FIELD_STATUS_OK && row.statics.parentPid === selfPid;
}

/**
* True for a session-critical process that must never be signaled: PID 0/1
* plus the {@link CRITICAL_PROCESS_NAMES} denylist, matched against both the
Expand Down Expand Up @@ -123,7 +135,7 @@ export function disabledReasonFor(
if (!hasStableTargetIdentity(target)) {
return ActionDisabledReason.ACTION_DISABLED_REASON_UNSTABLE_IDENTITY;
}
if ((row.identity?.pid ?? 0) === selfPid) {
if (isSelfProcess(row, selfPid)) {
return ActionDisabledReason.ACTION_DISABLED_REASON_SELF;
}
if (isCriticalProcess(row)) {
Expand Down
6 changes: 3 additions & 3 deletions src/renderer/components/metrics/cpu-graph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { useRef, type PointerEvent as ReactPointerEvent } from "react";
import { cn } from "@/lib/utils";
import type { MetricState } from "@/domain/metric-view";
import { areaRuns } from "@/domain/area-path";
import { HISTORY_CAPACITY, sampleIndexAtFraction } from "@/domain/sample-history";
import { HISTORY_CAPACITY, sampleIndexAtFraction, type HistorySample } from "@/domain/sample-history";
import { AreaLayer, Baseline, ScrubBand } from "@/components/metrics/area-layer";

/** A 0-100 percent reading, or `null` for a tick whose reading was not OK. */
export type CpuSample = number | null;
/** A CPU percent reading, or `null` for a tick whose reading was not OK. */
export type CpuSample = HistorySample;

const FILL_BY_STATE: Record<MetricState, string> = {
ok: "text-success",
Expand Down
54 changes: 54 additions & 0 deletions src/renderer/components/metrics/memory-graph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useRef, type PointerEvent as ReactPointerEvent } from "react";

import { areaRuns } from "@/domain/area-path";
import { HISTORY_CAPACITY, sampleIndexAtFraction, type HistorySample } from "@/domain/sample-history";
import { AreaLayer, Baseline, ScrubBand } from "@/components/metrics/area-layer";

const PEAK = 88;
const BASELINE_Y = 99.5;
// Keep page-sized memory wobble from turning into a dramatic spike.
const MIN_SPAN_BYTES = 16 * 1024 * 1024;

/** Floating-axis memory trend for one process or group. */
export function MemoryGraph({
history,
scrubIndex,
onScrub,
}: {
history: HistorySample[];
scrubIndex: number | null;
onScrub: (index: number | null) => void;
}) {
const ref = useRef<SVGSVGElement>(null);
const offset = HISTORY_CAPACITY - history.length;

const values = history.filter((sample): sample is number => sample !== null);
const max = values.length > 0 ? Math.max(...values) : 0;
const min = values.length > 0 ? Math.min(...values) : 0;
const base = Math.max(0, min - MIN_SPAN_BYTES * 0.25);
const span = Math.max(MIN_SPAN_BYTES, max - base);
const runs = areaRuns(history, offset, (sample) => ((sample - base) / span) * PEAK, 100, -1);

const handleMove = (event: ReactPointerEvent<SVGSVGElement>) => {
const rect = ref.current?.getBoundingClientRect();
if (!rect || rect.width === 0) return;
onScrub(sampleIndexAtFraction((event.clientX - rect.left) / rect.width, history.length));
};

return (
<svg
ref={ref}
viewBox={`0 0 ${HISTORY_CAPACITY} 100`}
preserveAspectRatio="none"
className="h-full w-full text-mem-app"
role="img"
aria-label="Recent memory footprint"
onPointerMove={handleMove}
onPointerLeave={() => onScrub(null)}
>
<Baseline y={BASELINE_Y} offset={offset} />
<AreaLayer runs={runs} />
{scrubIndex !== null ? <ScrubBand x={offset + scrubIndex} /> : null}
</svg>
);
}
13 changes: 3 additions & 10 deletions src/renderer/components/processes/disclosure.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,7 @@ import { appGateway } from "@/gateway/app-gateway";
import { cn } from "@/lib/utils";

/**
* Reusable disclosure and copy primitives for the detail view. Sensitive
* process text (paths, argv) is copied only on explicit user action and
* routes through main because the renderer is sandboxed.
*/

/**
* Smooth height/opacity wrapper for disclosure bodies. Content stays mounted
* so close animations can run; `inert` keeps hidden controls unreachable.
* Smooth height/opacity wrapper for disclosure bodies.
*/
export function DisclosureContent({
open,
Expand All @@ -26,8 +19,8 @@ export function DisclosureContent({
aria-hidden={!open}
inert={open ? undefined : true}
className={cn(
"grid transition-[grid-template-rows,opacity,margin-top] duration-150 ease-out motion-reduce:transition-none",
open ? "mt-1.5 grid-rows-[1fr] opacity-100" : "mt-0 grid-rows-[0fr] opacity-0",
"grid transition-[grid-template-rows,opacity] duration-150 ease-out motion-reduce:transition-none",
open ? "grid-rows-[1fr] opacity-100" : "grid-rows-[0fr] opacity-0",
)}
>
<div className="min-h-0 overflow-hidden">{children}</div>
Expand Down
70 changes: 70 additions & 0 deletions src/renderer/components/processes/member-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { memo } from "react";

import { cn } from "@/lib/utils";
import { ProcessIcon } from "@/components/processes/process-icon";
import { metricValueText } from "@/domain/process-list";
import type { DetailMember } from "@/domain/process-detail";

/**
* One member-process row - icon, name, active-metric value - drillable into its
* own detail. Shared by the detail view's Members section and the inline
* expanded list under a grouped row, where `indented` sits the row under the
* parent icon and mutes its name to read as a child. Memoized field-wise so an
* unchanged member skips re-rendering across snapshot ticks.
*/
export const MemberRow = memo(
function MemberRow({
member,
indented = false,
onOpen,
}: {
member: DetailMember
indented?: boolean
onOpen: (pid: number, startedAtUnixMs?: number) => void
}) {
return (
<button
type="button"
onClick={() => onOpen(member.pid, member.startedAtUnixMs)}
aria-label={`Show details for ${member.name}, PID ${member.pid}`}
title={`${member.name} - PID ${member.pid}`}
className={cn(
"flex h-9 w-full items-center gap-2.5 rounded-md pr-1 text-left transition-colors hover:bg-muted/50 focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-inset focus-visible:ring-ring",
indented ? "pl-6" : "pl-2",
)}
>
<ProcessIcon iconPngBase64={member.iconPngBase64} name={member.name} />
<span
className={cn(
"min-w-0 flex-1 truncate text-[12px]",
member.notResponding
? "text-destructive"
: indented
? "text-muted-foreground"
: "text-foreground",
)}
>
{member.name}
</span>
<span
className={cn(
"shrink-0 whitespace-nowrap text-right text-[12px] tabular-nums",
member.metricState === "ok" ? "text-foreground" : "text-muted-foreground",
)}
>
{metricValueText(member.metricState, member.metricText)}
</span>
</button>
);
},
(prev, next) =>
prev.onOpen === next.onOpen &&
prev.indented === next.indented &&
prev.member.pid === next.member.pid &&
prev.member.startedAtUnixMs === next.member.startedAtUnixMs &&
prev.member.name === next.member.name &&
prev.member.iconPngBase64 === next.member.iconPngBase64 &&
prev.member.metricState === next.member.metricState &&
prev.member.metricText === next.member.metricText &&
prev.member.notResponding === next.member.notResponding,
);
13 changes: 6 additions & 7 deletions src/renderer/components/processes/process-actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,12 @@ export function ProcessActions({
);

return (
<div className="flex flex-col gap-2 border-t border-border/60 pt-3">
<p
className={cn("min-h-4 text-[11px] text-muted-foreground", message ? undefined : "invisible")}
role="status"
>
{message}
</p>
<div className="flex flex-col gap-2 border-t border-border/60 pt-2">
{message ? (
<p className="text-[11px] text-muted-foreground" role="status">
{message}
</p>
) : null}
<div className="flex items-center gap-2">
<ActionButton
label="Open"
Expand Down
Loading
Loading